* Add all HTMLDiff-related classes to the autoloader.
authorChad Horohoe <demon@users.mediawiki.org>
Mon, 25 Aug 2008 20:19:00 +0000 (20:19 +0000)
committerChad Horohoe <demon@users.mediawiki.org>
Mon, 25 Aug 2008 20:19:00 +0000 (20:19 +0000)
* Move all stuff @ingroup DifferenceEngine to /diff
* Split off Node and friends to Nodes.php to make HTMLDiff.php shorter.

includes/AutoLoader.php
includes/Diff.php [deleted file]
includes/DifferenceEngine.php [deleted file]
includes/HTMLDiff.php [deleted file]
includes/diff/Diff.php [new file with mode: 0644]
includes/diff/DifferenceEngine.php [new file with mode: 0644]
includes/diff/HTMLDiff.php [new file with mode: 0644]
includes/diff/Nodes.php [new file with mode: 0644]

index d781f3f..8d01ae2 100644 (file)
@@ -14,7 +14,6 @@ $wgAutoloadLocalClasses = array(
        'AjaxResponse' => 'includes/AjaxResponse.php',
        'AlphabeticPager' => 'includes/Pager.php',
        'APCBagOStuff' => 'includes/BagOStuff.php',
-       'ArrayDiffFormatter' => 'includes/DifferenceEngine.php',
        'Article' => 'includes/Article.php',
        'AtomFeed' => 'includes/Feed.php',
        'AuthPlugin' => 'includes/AuthPlugin.php',
@@ -34,17 +33,7 @@ $wgAutoloadLocalClasses = array(
        'CreativeCommonsRdf' => 'includes/Metadata.php',
        'Credits' => 'includes/Credits.php',
        'DBABagOStuff' => 'includes/BagOStuff.php',
-       'DelegatingContentHandler' => 'includes/HTMLDiff.php',
        'DependencyWrapper' => 'includes/CacheDependency.php',
-       '_DiffEngine' => 'includes/DifferenceEngine.php',
-       'DifferenceEngine' => 'includes/DifferenceEngine.php',
-       'DiffFormatter' => 'includes/DifferenceEngine.php',
-       'Diff' => 'includes/DifferenceEngine.php',
-       '_DiffOp_Add' => 'includes/DifferenceEngine.php',
-       '_DiffOp_Change' => 'includes/DifferenceEngine.php',
-       '_DiffOp_Copy' => 'includes/DifferenceEngine.php',
-       '_DiffOp_Delete' => 'includes/DifferenceEngine.php',
-       '_DiffOp' => 'includes/DifferenceEngine.php',
        'DjVuImage' => 'includes/DjVuImage.php',
        'DoubleReplacer' => 'includes/StringUtils.php',
        'DoubleRedirectJob' => 'includes/DoubleRedirectJob.php',
@@ -61,7 +50,6 @@ $wgAutoloadLocalClasses = array(
        'DumpOutput' => 'includes/Export.php',
        'DumpPipeOutput' => 'includes/Export.php',
        'eAccelBagOStuff' => 'includes/BagOStuff.php',
-       'EchoingContentHandler' => 'includes/HTMLDiff.php',
        'EditPage' => 'includes/EditPage.php',
        'EmaillingJob' => 'includes/EmaillingJob.php',
        'EmailNotification' => 'includes/UserMailer.php',
@@ -95,11 +83,8 @@ $wgAutoloadLocalClasses = array(
        'HistoryBlobStub' => 'includes/HistoryBlob.php',
        'HTMLCacheUpdate' => 'includes/HTMLCacheUpdate.php',
        'HTMLCacheUpdateJob' => 'includes/HTMLCacheUpdate.php',
-       'HTMLDiffer' => 'includes/HTMLDiff.php',
        'HTMLFileCache' => 'includes/HTMLFileCache.php',
-       'HTMLOutput' => 'includes/HTMLDiff.php',
        'Http' => 'includes/HttpFunctions.php',
-       '_HWLDF_WordAccumulator' => 'includes/DifferenceEngine.php',
        'ImageGallery' => 'includes/ImageGallery.php',
        'ImageHistoryList' => 'includes/ImagePage.php',
        'ImagePage' => 'includes/ImagePage.php',
@@ -124,7 +109,6 @@ $wgAutoloadLocalClasses = array(
        'MagicWordArray' => 'includes/MagicWord.php',
        'MagicWord' => 'includes/MagicWord.php',
        'MailAddress' => 'includes/UserMailer.php',
-       'MappedDiff' => 'includes/DifferenceEngine.php',
        'MathRenderer' => 'includes/Math.php',
        'MediaTransformError' => 'includes/MediaTransformOutput.php',
        'MediaTransformOutput' => 'includes/MediaTransformOutput.php',
@@ -157,7 +141,6 @@ $wgAutoloadLocalClasses = array(
        'ProtectionForm' => 'includes/ProtectionForm.php',
        'QueryPage' => 'includes/QueryPage.php',
        'QuickTemplate' => 'includes/SkinTemplate.php',
-       'RangeDifference' => 'includes/Diff.php',
        'RawPage' => 'includes/RawPage.php',
        'RCCacheEntry' => 'includes/ChangesList.php',
        'RdfMetaData' => 'includes/Metadata.php',
@@ -196,7 +179,6 @@ $wgAutoloadLocalClasses = array(
        'SquidUpdate' => 'includes/SquidUpdate.php',
        'Status' => 'includes/Status.php',
        'StringUtils' => 'includes/StringUtils.php',
-       'TableDiffFormatter' => 'includes/DifferenceEngine.php',
        'TablePager' => 'includes/Pager.php',
        'ThumbnailImage' => 'includes/MediaTransformOutput.php',
        'TitleDependency' => 'includes/CacheDependency.php',
@@ -205,7 +187,6 @@ $wgAutoloadLocalClasses = array(
        'TitleListDependency' => 'includes/CacheDependency.php',
        'TransformParameterError' => 'includes/MediaTransformOutput.php',
        'TurckBagOStuff' => 'includes/BagOStuff.php',
-       'UnifiedDiffFormatter' => 'includes/DifferenceEngine.php',
        'UnlistedSpecialPage' => 'includes/SpecialPage.php',
        'User' => 'includes/User.php',
        'UserArray' => 'includes/UserArray.php',
@@ -216,12 +197,10 @@ $wgAutoloadLocalClasses = array(
        'WatchlistEditor' => 'includes/WatchlistEditor.php',
        'WebRequest' => 'includes/WebRequest.php',
        'WebResponse' => 'includes/WebResponse.php',
-       'WikiDiff3' => 'includes/Diff.php',
        'WikiError' => 'includes/WikiError.php',
        'WikiErrorMsg' => 'includes/WikiError.php',
        'WikiExporter' => 'includes/Export.php',
        'WikiXmlError' => 'includes/WikiError.php',
-       'WordLevelDiff' => 'includes/DifferenceEngine.php',
        'XCacheBagOStuff' => 'includes/BagOStuff.php',
        'XmlDumpWriter' => 'includes/Export.php',
        'Xml' => 'includes/Xml.php',
@@ -328,6 +307,49 @@ $wgAutoloadLocalClasses = array(
        'ResultWrapper' => 'includes/db/Database.php',
        'SQLiteField' => 'includes/db/DatabaseSqlite.php',
 
+       # includes/diff
+       'AncestorComparator' => 'includes/diff/HTMLDiff.php',
+       'AncestorComparatorResult' => 'includes/diff/HTMLDiff.php',
+       'AnchorToString' => 'includes/diff/HTMLDiff.php',
+       'ArrayDiffFormatter' => 'includes/diff/DifferenceEngine.php',
+       'BodyNode' => 'includes/diff/Nodes.php',
+       'ChangeText' => 'includes/diff/HTMLDiff.php',
+       'ChangeTextGenerator' => 'includes/diff/HTMLDiff.php',
+       'DelegatingContentHandler' => 'includes/diff/HTMLDiff.php',
+       '_DiffEngine' => 'includes/diff/DifferenceEngine.php',
+       'DifferenceEngine' => 'includes/diff/DifferenceEngine.php',
+       'DiffFormatter' => 'includes/diff/DifferenceEngine.php',
+       'Diff' => 'includes/diff/DifferenceEngine.php',
+       '_DiffOp_Add' => 'includes/diff/DifferenceEngine.php',
+       '_DiffOp_Change' => 'includes/diff/DifferenceEngine.php',
+       '_DiffOp_Copy' => 'includes/diff/DifferenceEngine.php',
+       '_DiffOp_Delete' => 'includes/diff/DifferenceEngine.php',
+       '_DiffOp' => 'includes/diff/DifferenceEngine.php',
+       'DomTreeBuilder' => 'includes/diff/HTMLDiff.php',
+       'DummyNode' => 'includes/diff/Nodes.php',
+       'EchoingContentHandler' => 'includes/diff/HTMLDiff.php',
+       'HTMLDiffer' => 'includes/diff/HTMLDiff.php',
+       'HTMLOutput' => 'includes/diff/HTMLDiff.php',
+       '_HWLDF_WordAccumulator' => 'includes/diff/DifferenceEngine.php',
+       'ImageNode' => 'includes/diff/Nodes.php',
+       'LastCommonParentResult' => 'includes/diff/HTMLDiff.php',
+       'MappedDiff' => 'includes/diff/DifferenceEngine.php',
+       'Modification' => 'includes/diff/HTMLDiff.php',
+       'NoContentTagToString' => 'includes/diff/HTMLDiff.php',
+       'Node' => 'includes/diff/Nodes.php',
+       'RangeDifference' => 'includes/diff/Diff.php',
+       'TableDiffFormatter' => 'includes/diff/DifferenceEngine.php',
+       'TagNode' => 'includes/diff/Nodes.php',
+       'TagToString' => 'includes/diff/HTMLDiff.php',
+       'TagToStringFactory' => 'includes/diff/HTMLDiff.php',
+       'TextNode' => 'includes/diff/Nodes.php',
+       'TextNodeDiffer' => 'includes/diff/HTMLDiff.php',
+       'TextOnlyComparator' => 'includes/diff/HTMLDiff.php',
+       'UnifiedDiffFormatter' => 'includes/diff/DifferenceEngine.php',
+       'WhiteSpaceNode' => 'includes/diff/Nodes.php',
+       'WikiDiff3' => 'includes/diff/Diff.php',
+       'WordLevelDiff' => 'includes/diff/DifferenceEngine.php',
+       
        # includes/filerepo
        'ArchivedFile' => 'includes/filerepo/ArchivedFile.php',
        'File' => 'includes/filerepo/File.php',
diff --git a/includes/Diff.php b/includes/Diff.php
deleted file mode 100644 (file)
index 538c2d8..0000000
+++ /dev/null
@@ -1,580 +0,0 @@
-<?php
-/* Copyright (C) 2008 Guy Van den Broeck <guy@guyvdb.eu>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
- * or see http://www.gnu.org/
- */
-
-/**
- * This diff implementation is mainly lifted from the LCS algorithm of the Eclipse project which
- * in turn is based on Myers' "An O(ND) difference algorithm and its variations"
- * (http://citeseer.ist.psu.edu/myers86ond.html) with range compression (see Wu et al.'s
- * "An O(NP) Sequence Comparison Algorithm").
- *
- * This implementation supports an upper bound on the excution time.
- *
- * Complexity: O((M + N)D) worst case time, O(M + N + D^2) expected time, O(M + N) space
- *
- * @author Guy Van den Broeck
- * @ingroup DifferenceEngine
- */
-class WikiDiff3 {
-
-       //Input variables
-       private $from;
-       private $to;
-       private $m;
-       private $n;
-
-       private $tooLong;
-       private $powLimit;
-
-       //State variables
-       private $maxDifferences;
-       private $lcsLengthCorrectedForHeuristic = false;
-
-       //Output variables
-       public $length;
-       public $removed;
-       public $added;
-       public $heuristicUsed;
-
-       function __construct($tooLong = 2000000, $powLimit = 1.45){
-               $this->tooLong = $tooLong;
-               $this->powLimit = $powLimit;
-       }
-
-       public function diff(/*array*/ $from, /*array*/ $to){
-               //remember initial lengths
-               $m = sizeof($from);
-               $n = count($to);
-
-               $this->heuristicUsed = false;
-
-               //output
-               $removed = $m > 0 ? array_fill(0, $m, true) : array();
-               $added = $n > 0 ? array_fill(0, $n, true) : array();
-
-               //reduce the complexity for the next step (intentionally done twice)
-               //remove common tokens at the start
-               $i = 0;
-               while($i < $m && $i < $n && $from[$i] === $to[$i]) {
-                       $removed[$i] = $added[$i] = false;
-                       unset($from[$i], $to[$i]);
-                       ++$i;
-               }
-
-               //remove common tokens at the end
-               $j = 1;
-               while($i + $j <= $m && $i + $j <= $n && $from[$m - $j] === $to[$n - $j]) {
-                       $removed[$m - $j] = $added[$n - $j] = false;
-                       unset($from[$m - $j], $to[$n - $j]);
-                       ++$j;
-               }
-
-               $this->from = $newFromIndex = $this->to = $newToIndex = array();
-
-               //remove tokens not in both sequences
-               $shared = array();
-               foreach( $from as $key ) {
-                       $shared[$key] = false;
-               }
-
-               foreach($to as $index => &$el) {
-                       if(array_key_exists($el, $shared)) {
-                               //keep it
-                               $this->to[] = $el;
-                               $shared[$el] = true;
-                               $newToIndex[] = $index;
-                       }
-               }
-               foreach($from as $index => &$el) {
-                       if($shared[$el]) {
-                               //keep it
-                               $this->from[] = $el;
-                               $newFromIndex[] = $index;
-                       }
-               }
-
-               unset($shared, $from, $to);
-
-               $this->m = count($this->from);
-               $this->n = count($this->to);
-
-               $this->removed = $this->m > 0 ? array_fill(0, $this->m, true) : array();
-               $this->added = $this->n > 0 ? array_fill(0, $this->n, true) : array();
-
-               if ($this->m == 0 || $this->n == 0) {
-                       $this->length = 0;
-               } else {
-                       $this->maxDifferences = ceil(($this->m + $this->n) / 2.0);
-                       if ($this->m * $this->n > $this->tooLong) {
-                               // limit complexity to D^POW_LIMIT for long sequences
-                               $this->maxDifferences = floor(pow($this->maxDifferences, $this->powLimit - 1.0));
-                               wfDebug("Limiting max number of differences to $this->maxDifferences\n");
-                       }
-
-                       /*
-                        * The common prefixes and suffixes are always part of some LCS, include
-                        * them now to reduce our search space
-                        */
-                       $max = min($this->m, $this->n);
-                       for ($forwardBound = 0; $forwardBound < $max
-                                       && $this->from[$forwardBound] === $this->to[$forwardBound];
-                                       ++$forwardBound) {
-                               $this->removed[$forwardBound] = $this->added[$forwardBound] = false;
-                       }
-
-                       $backBoundL1 = $this->m - 1;
-                       $backBoundL2 = $this->n - 1;
-
-                       while ($backBoundL1 >= $forwardBound && $backBoundL2 >= $forwardBound
-                                       && $this->from[$backBoundL1] === $this->to[$backBoundL2]) {
-                               $this->removed[$backBoundL1--] = $this->added[$backBoundL2--] = false;
-                       }
-
-                       $temp = array_fill(0, $this->m + $this->n + 1, 0);
-                       $V = array($temp, $temp);
-                       $snake = array(0, 0, 0);
-
-                       $this->length = $forwardBound + $this->m - $backBoundL1 - 1
-                               + $this->lcs_rec($forwardBound, $backBoundL1,
-                               $forwardBound, $backBoundL2, $V, $snake);
-               }
-
-               $this->m = $m;
-               $this->n = $n;
-
-               $this->length += $i + $j - 1;
-
-               foreach($this->removed as $key => &$removed_elem) {
-                       if(!$removed_elem) {
-                               $removed[$newFromIndex[$key]] = false;
-                       }
-               }
-               foreach($this->added as $key => &$added_elem) {
-                       if(!$added_elem) {
-                               $added[$newToIndex[$key]] = false;
-                       }
-               }
-               $this->removed = $removed;
-               $this->added = $added;
-       }
-
-       function diff_range($from_lines, $to_lines) {
-               // Diff and store locally
-               $this->diff($from_lines, $to_lines);
-               unset($from_lines, $to_lines);
-
-               $ranges = array();
-               $xi = $yi = 0;
-               while ($xi < $this->m || $yi < $this->n) {
-                       // Matching "snake".
-                       while ($xi < $this->m && $yi < $this->n
-                                       && !$this->removed[$xi]
-                                       && !$this->added[$yi]) {
-                               ++$xi;
-                               ++$yi;
-                       }
-                       // Find deletes & adds.
-                       $xstart = $xi;
-                       while ($xi < $this->m && $this->removed[$xi]) {
-                               ++$xi;
-                       }
-
-                       $ystart = $yi;
-                       while ($yi < $this->n && $this->added[$yi]) {
-                               ++$yi;
-                       }
-
-                       if ($xi > $xstart || $yi > $ystart) {
-                               $ranges[] = new RangeDifference($xstart, $xi,
-                                                               $ystart, $yi);
-                       }
-               }
-               return $ranges;
-       }
-
-       private function lcs_rec($bottoml1, $topl1, $bottoml2, $topl2, &$V, &$snake) {
-               // check that both sequences are non-empty
-               if ($bottoml1 > $topl1 || $bottoml2 > $topl2) {
-                       return 0;
-               }
-
-               $d = $this->find_middle_snake($bottoml1, $topl1, $bottoml2,
-                                                       $topl2, $V, $snake);
-
-               // need to store these so we don't lose them when they're
-               // overwritten by the recursion
-               $len = $snake[2];
-               $startx = $snake[0];
-               $starty = $snake[1];
-
-               // the middle snake is part of the LCS, store it
-               for ($i = 0; $i < $len; ++$i) {
-                       $this->removed[$startx + $i] = $this->added[$starty + $i] = false;
-               }
-
-               if ($d > 1) {
-                       return $len
-                       + $this->lcs_rec($bottoml1, $startx - 1, $bottoml2,
-                                                       $starty - 1, $V, $snake)
-                       + $this->lcs_rec($startx + $len, $topl1, $starty + $len,
-                                                       $topl2, $V, $snake);
-               } else if ($d == 1) {
-                       /*
-                        * In this case the sequences differ by exactly 1 line. We have
-                        * already saved all the lines after the difference in the for loop
-                        * above, now we need to save all the lines before the difference.
-                        */
-                       $max = min($startx - $bottoml1, $starty - $bottoml2);
-                       for ($i = 0; $i < $max; ++$i) {
-                               $this->removed[$bottoml1 + $i] =
-                                       $this->added[$bottoml2 + $i] = false;
-                       }
-                       return $max + $len;
-               }
-               return $len;
-       }
-
-       private function find_middle_snake($bottoml1, $topl1, $bottoml2,$topl2, &$V, &$snake) {
-               $from = &$this->from;
-               $to = &$this->to;
-               $V0 = &$V[0];
-               $V1 = &$V[1];
-               $snake0 = &$snake[0];
-               $snake1 = &$snake[1];
-               $snake2 = &$snake[2];
-               $bottoml1_min_1 = $bottoml1-1;
-               $bottoml2_min_1 = $bottoml2-1;
-               $N = $topl1 - $bottoml1_min_1;
-               $M = $topl2 - $bottoml2_min_1;
-               $delta = $N - $M;
-               $maxabsx = $N+$bottoml1;
-               $maxabsy = $M+$bottoml2;
-               $limit = min($this->maxDifferences, ceil(($N + $M ) / 2));
-
-               //value_to_add_forward: a 0 or 1 that we add to the start
-               // offset to make it odd/even
-               if (($M & 1) == 1) {
-                       $value_to_add_forward = 1;
-               } else {
-                       $value_to_add_forward = 0;
-               }
-
-               if (($N & 1) == 1) {
-                       $value_to_add_backward = 1;
-               } else {
-                       $value_to_add_backward = 0;
-               }
-
-               $start_forward = -$M;
-               $end_forward = $N;
-               $start_backward = -$N;
-               $end_backward = $M;
-
-               $limit_min_1 = $limit - 1;
-               $limit_plus_1 = $limit + 1;
-
-               $V0[$limit_plus_1] = 0;
-               $V1[$limit_min_1] = $N;
-               $limit = min($this->maxDifferences, ceil(($N + $M ) / 2));
-
-               if (($delta & 1) == 1) {
-                       for ($d = 0; $d <= $limit; ++$d) {
-                               $start_diag = max($value_to_add_forward + $start_forward, -$d);
-                               $end_diag = min($end_forward, $d);
-                               $value_to_add_forward = 1 - $value_to_add_forward;
-
-                               // compute forward furthest reaching paths
-                               for ($k = $start_diag; $k <= $end_diag; $k += 2) {
-                                       if ($k == -$d || ($k < $d
-                                                       && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k])) {
-                                               $x = $V0[$limit_plus_1 + $k];
-                                       } else {
-                                               $x = $V0[$limit_min_1 + $k] + 1;
-                                       }
-
-                                       $absx = $snake0 = $x + $bottoml1;
-                                       $absy = $snake1 = $x - $k + $bottoml2;
-
-                                       while ($absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy]) {
-                                               ++$absx;
-                                               ++$absy;
-                                       }
-                                       $x = $absx-$bottoml1;
-
-                                       $snake2 = $absx -$snake0;
-                                       $V0[$limit + $k] = $x;
-                                       if ($k >= $delta - $d + 1 && $k <= $delta + $d - 1
-                                                       && $x >= $V1[$limit + $k - $delta]) {
-                                               return 2 * $d - 1;
-                                       }
-
-                                       // check to see if we can cut down the diagonal range
-                                       if ($x >= $N && $end_forward > $k - 1) {
-                                               $end_forward = $k - 1;
-                                       } else if ($absy - $bottoml2 >= $M) {
-                                               $start_forward = $k + 1;
-                                               $value_to_add_forward = 0;
-                                       }
-                               }
-
-                               $start_diag = max($value_to_add_backward + $start_backward, -$d);
-                               $end_diag = min($end_backward, $d);
-                               $value_to_add_backward = 1 - $value_to_add_backward;
-
-                               // compute backward furthest reaching paths
-                               for ($k = $start_diag; $k <= $end_diag; $k += 2) {
-                                       if ($k == $d
-                                       || ($k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k])) {
-                                               $x = $V1[$limit_min_1 + $k];
-                                       } else {
-                                               $x = $V1[$limit_plus_1 + $k] - 1;
-                                       }
-
-                                       $y = $x - $k - $delta;
-
-                                       $snake2 = 0;
-                                       while ($x > 0 && $y > 0
-                                       && $from[$x +$bottoml1_min_1] === $to[$y + $bottoml2_min_1]) {
-                                               --$x;
-                                               --$y;
-                                               ++$snake2;
-                                       }
-                                       $V1[$limit + $k] = $x;
-
-                                       // check to see if we can cut down our diagonal range
-                                       if ($x <= 0) {
-                                               $start_backward = $k + 1;
-                                               $value_to_add_backward = 0;
-                                       } else if ($y <= 0 && $end_backward > $k - 1) {
-                                               $end_backward = $k - 1;
-                                       }
-                               }
-                       }
-               } else {
-                       for ($d = 0; $d <= $limit; ++$d) {
-                               $start_diag = max($value_to_add_forward + $start_forward, -$d);
-                               $end_diag = min($end_forward, $d);
-                               $value_to_add_forward = 1 - $value_to_add_forward;
-
-                               // compute forward furthest reaching paths
-                               for ($k = $start_diag; $k <= $end_diag; $k += 2) {
-                                       if ($k == -$d
-                                       || ($k < $d && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k])) {
-                                               $x = $V0[$limit_plus_1 + $k];
-                                       } else {
-                                               $x = $V0[$limit_min_1 + $k] + 1;
-                                       }
-
-                                       $absx = $snake0 = $x + $bottoml1;
-                                       $absy = $snake1 = $x - $k + $bottoml2;
-
-                                       while ($absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy]) {
-                                               ++$absx;
-                                               ++$absy;
-                                       }
-                                       $x = $absx-$bottoml1;
-                                       $snake2 = $absx -$snake0;
-                                       $V0[$limit + $k] = $x;
-
-                                       // check to see if we can cut down the diagonal range
-                                       if ($x >= $N && $end_forward > $k - 1) {
-                                               $end_forward = $k - 1;
-                                       } else if ($absy-$bottoml2 >= $M) {
-                                               $start_forward = $k + 1;
-                                               $value_to_add_forward = 0;
-                                       }
-                               }
-
-                               $start_diag = max($value_to_add_backward + $start_backward, -$d);
-                               $end_diag = min($end_backward, $d);
-                               $value_to_add_backward = 1 - $value_to_add_backward;
-
-                               // compute backward furthest reaching paths
-                               for ($k = $start_diag; $k <= $end_diag; $k += 2) {
-                                       if ($k == $d
-                                       || ($k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k])) {
-                                               $x = $V1[$limit_min_1 + $k];
-                                       } else {
-                                               $x = $V1[$limit_plus_1 + $k] - 1;
-                                       }
-
-                                       $y = $x - $k - $delta;
-
-                                       $snake2 = 0;
-                                       while ($x > 0 && $y > 0
-                                                       && $from[$x +$bottoml1_min_1] === $to[$y + $bottoml2_min_1]) {
-                                               --$x;
-                                               --$y;
-                                               ++$snake2;
-                                       }
-                                       $V1[$limit + $k] = $x;
-
-                                       if ($k >= -$delta - $d && $k <= $d - $delta
-                                                       && $x <= $V0[$limit + $k + $delta]) {
-                                               $snake0 = $bottoml1 + $x;
-                                               $snake1 = $bottoml2 + $y;
-                                               return 2 * $d;
-                                       }
-
-                                       // check to see if we can cut down our diagonal range
-                                       if ($x <= 0) {
-                                               $start_backward = $k + 1;
-                                               $value_to_add_backward = 0;
-                                       } else if ($y <= 0 && $end_backward > $k - 1) {
-                                               $end_backward = $k - 1;
-                                       }
-                               }
-                       }
-               }
-               /*
-                * computing the true LCS is too expensive, instead find the diagonal
-                * with the most progress and pretend a midle snake of length 0 occurs
-                * there.
-                */
-
-               $most_progress = self::findMostProgress($M, $N, $limit, $V);
-
-               $snake0 = $bottoml1 + $most_progress[0];
-               $snake1 = $bottoml2 + $most_progress[1];
-               $snake2 = 0;
-               wfDebug("Computing the LCS is too expensive. Using a heuristic.\n");
-               $this->heuristicUsed = true;
-               return 5; /*
-               * HACK: since we didn't really finish the LCS computation
-               * we don't really know the length of the SES. We don't do
-               * anything with the result anyway, unless it's <=1. We know
-               * for a fact SES > 1 so 5 is as good a number as any to
-               * return here
-               */
-       }
-
-       private static function findMostProgress($M, $N, $limit, $V) {
-               $delta = $N - $M;
-
-               if (($M & 1) == ($limit & 1)) {
-                       $forward_start_diag = max(-$M, -$limit);
-               } else {
-                       $forward_start_diag = max(1 - $M, -$limit);
-               }
-
-               $forward_end_diag = min($N, $limit);
-
-               if (($N & 1) == ($limit & 1)) {
-                       $backward_start_diag = max(-$N, -$limit);
-               } else {
-                       $backward_start_diag = max(1 - $N, -$limit);
-               }
-
-               $backward_end_diag = -min($M, $limit);
-
-               $temp = array(0, 0, 0);
-
-
-               $max_progress = array_fill(0, ceil(max($forward_end_diag - $forward_start_diag,
-                               $backward_end_diag - $backward_start_diag) / 2), $temp);
-               $num_progress = 0; // the 1st entry is current, it is initialized
-               // with 0s
-
-               // first search the forward diagonals
-               for ($k = $forward_start_diag; $k <= $forward_end_diag; $k += 2) {
-                       $x = $V[0][$limit + $k];
-                       $y = $x - $k;
-                       if ($x > $N || $y > $M) {
-                               continue;
-                       }
-
-                       $progress = $x + $y;
-                       if ($progress > $max_progress[0][2]) {
-                               $num_progress = 0;
-                               $max_progress[0][0] = $x;
-                               $max_progress[0][1] = $y;
-                               $max_progress[0][2] = $progress;
-                       } else if ($progress == $max_progress[0][2]) {
-                               ++$num_progress;
-                               $max_progress[$num_progress][0] = $x;
-                               $max_progress[$num_progress][1] = $y;
-                               $max_progress[$num_progress][2] = $progress;
-                       }
-               }
-
-               $max_progress_forward = true; // initially the maximum
-               // progress is in the forward
-               // direction
-
-               // now search the backward diagonals
-               for ($k = $backward_start_diag; $k <= $backward_end_diag; $k += 2) {
-                       $x = $V[1][$limit + $k];
-                       $y = $x - $k - $delta;
-                       if ($x < 0 || $y < 0) {
-                               continue;
-                       }
-
-                       $progress = $N - $x + $M - $y;
-                       if ($progress > $max_progress[0][2]) {
-                               $num_progress = 0;
-                               $max_progress_forward = false;
-                               $max_progress[0][0] = $x;
-                               $max_progress[0][1] = $y;
-                               $max_progress[0][2] = $progress;
-                       } else if ($progress == $max_progress[0][2] && !$max_progress_forward) {
-                               ++$num_progress;
-                               $max_progress[$num_progress][0] = $x;
-                               $max_progress[$num_progress][1] = $y;
-                               $max_progress[$num_progress][2] = $progress;
-                       }
-               }
-
-               // return the middle diagonal with maximal progress.
-               return $max_progress[floor($num_progress / 2)];
-       }
-
-       public function getLcsLength(){
-               if($this->heuristicUsed && !$this->lcsLengthCorrectedForHeuristic){
-                       $this->lcsLengthCorrectedForHeuristic = true;
-                       $this->length = $this->m-array_sum($this->added);
-               }
-               return $this->length;
-       }
-
-}
-
-/**
- * Alternative representation of a set of changes, by the index
- * ranges that are changed.
- * 
- * @ingroup DifferenceEngine
- */
-class RangeDifference {
-
-       public $leftstart;
-       public $leftend;
-       public $leftlength;
-
-       public $rightstart;
-       public $rightend;
-       public $rightlength;
-
-       function __construct($leftstart, $leftend, $rightstart, $rightend){
-               $this->leftstart = $leftstart;
-               $this->leftend = $leftend;
-               $this->leftlength = $leftend - $leftstart;
-               $this->rightstart = $rightstart;
-               $this->rightend = $rightend;
-               $this->rightlength = $rightend - $rightstart;
-       }
-}
diff --git a/includes/DifferenceEngine.php b/includes/DifferenceEngine.php
deleted file mode 100644 (file)
index 0f03058..0000000
+++ /dev/null
@@ -1,2115 +0,0 @@
-<?php
-/**
- * @defgroup DifferenceEngine DifferenceEngine
- */
-
-/**
- * Constant to indicate diff cache compatibility.
- * Bump this when changing the diff formatting in a way that
- * fixes important bugs or such to force cached diff views to
- * clear.
- */
-define( 'MW_DIFF_VERSION', '1.11a' );
-
-/**
- * @todo document
- * @ingroup DifferenceEngine
- */
-class DifferenceEngine {
-       /**#@+
-        * @private
-        */
-       var $mOldid, $mNewid, $mTitle;
-       var $mOldtitle, $mNewtitle, $mPagetitle;
-       var $mOldtext, $mNewtext;
-       var $mOldPage, $mNewPage;
-       var $mRcidMarkPatrolled;
-       var $mOldRev, $mNewRev;
-       var $mRevisionsLoaded = false; // Have the revisions been loaded
-       var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
-       var $htmldiff;
-       /**#@-*/
-
-       /**
-        * Constructor
-        * @param $titleObj Title object that the diff is associated with
-        * @param $old Integer: old ID we want to show and diff with.
-        * @param $new String: either 'prev' or 'next'.
-        * @param $rcid Integer: ??? FIXME (default 0)
-        * @param $refreshCache boolean If set, refreshes the diff cache
-        * @param $htmldiff boolean If set, output using HTMLDiff instead of raw wikicode diff
-        */
-       function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false , $htmldiff = false) {
-               $this->mTitle = $titleObj;
-               wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
-
-               if ( 'prev' === $new ) {
-                       # Show diff between revision $old and the previous one.
-                       # Get previous one from DB.
-                       #
-                       $this->mNewid = intval($old);
-
-                       $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
-
-               } elseif ( 'next' === $new ) {
-                       # Show diff between revision $old and the previous one.
-                       # Get previous one from DB.
-                       #
-                       $this->mOldid = intval($old);
-                       $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid );
-                       if ( false === $this->mNewid ) {
-                               # if no result, NewId points to the newest old revision. The only newer
-                               # revision is cur, which is "0".
-                               $this->mNewid = 0;
-                       }
-
-               } else {
-                       $this->mOldid = intval($old);
-                       $this->mNewid = intval($new);
-               }
-               $this->mRcidMarkPatrolled = intval($rcid);  # force it to be an integer
-               $this->mRefreshCache = $refreshCache;
-               $this->htmldiff = $htmldiff;
-       }
-
-       function getTitle() {
-               return $this->mTitle;
-       }
-
-       function showDiffPage( $diffOnly = false ) {
-               global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol, $wgEnableHtmlDiff;
-               wfProfileIn( __METHOD__ );
-
-
-               # If external diffs are enabled both globally and for the user,
-               # we'll use the application/x-external-editor interface to call
-               # an external diff tool like kompare, kdiff3, etc.
-               if($wgUseExternalEditor && $wgUser->getOption('externaldiff')) {
-                       global $wgInputEncoding,$wgServer,$wgScript,$wgLang;
-                       $wgOut->disable();
-                       header ( "Content-type: application/x-external-editor; charset=".$wgInputEncoding );
-                       $url1=$this->mTitle->getFullURL("action=raw&oldid=".$this->mOldid);
-                       $url2=$this->mTitle->getFullURL("action=raw&oldid=".$this->mNewid);
-                       $special=$wgLang->getNsText(NS_SPECIAL);
-                       $control=<<<CONTROL
-                       [Process]
-                       Type=Diff text
-                       Engine=MediaWiki
-                       Script={$wgServer}{$wgScript}
-                       Special namespace={$special}
-
-                       [File]
-                       Extension=wiki
-                       URL=$url1
-
-                       [File 2]
-                       Extension=wiki
-                       URL=$url2
-CONTROL;
-                       echo($control);
-                       return;
-               }
-
-               $wgOut->setArticleFlag( false );
-               if ( ! $this->loadRevisionData() ) {
-                       $t = $this->mTitle->getPrefixedText();
-                       $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
-                       $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
-                       $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
-                       wfProfileOut( __METHOD__ );
-                       return;
-               }
-
-               wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
-
-               if ( $this->mNewRev->isCurrent() ) {
-                       $wgOut->setArticleFlag( true );
-               }
-
-               # mOldid is false if the difference engine is called with a "vague" query for
-               # a diff between a version V and its previous version V' AND the version V
-               # is the first version of that article. In that case, V' does not exist.
-               if ( $this->mOldid === false ) {
-                       $this->showFirstRevision();
-                       $this->renderNewRevision();  // should we respect $diffOnly here or not?
-                       wfProfileOut( __METHOD__ );
-                       return;
-               }
-
-               $wgOut->suppressQuickbar();
-
-               $oldTitle = $this->mOldPage->getPrefixedText();
-               $newTitle = $this->mNewPage->getPrefixedText();
-               if( $oldTitle == $newTitle ) {
-                       $wgOut->setPageTitle( $newTitle );
-               } else {
-                       $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
-               }
-               $wgOut->setSubtitle( wfMsg( 'difference' ) );
-               $wgOut->setRobotPolicy( 'noindex,nofollow' );
-
-               if ( !( $this->mOldPage->userCanRead() && $this->mNewPage->userCanRead() ) ) {
-                       $wgOut->loginToUse();
-                       $wgOut->output();
-                       wfProfileOut( __METHOD__ );
-                       exit;
-               }
-
-               $sk = $wgUser->getSkin();
-
-               // Check if page is editable
-               $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
-               if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) {
-                       $rollback = '&nbsp;&nbsp;&nbsp;' . $sk->generateRollback( $this->mNewRev );
-               } else {
-                       $rollback = '';
-               }
-
-               // Prepare a change patrol link, if applicable
-               if( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) {
-                       // If we've been given an explicit change identifier, use it; saves time
-                       if( $this->mRcidMarkPatrolled ) {
-                               $rcid = $this->mRcidMarkPatrolled;
-                       } else {
-                               // Look for an unpatrolled change corresponding to this diff
-                               $db = wfGetDB( DB_SLAVE );
-                               $change = RecentChange::newFromConds(
-                               array(
-                               // Add redundant user,timestamp condition so we can use the existing index
-                                               'rc_user_text'  => $this->mNewRev->getRawUserText(),
-                                               'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
-                                               'rc_this_oldid' => $this->mNewid,
-                                               'rc_last_oldid' => $this->mOldid,
-                                               'rc_patrolled' => 0
-                               ),
-                               __METHOD__
-                               );
-                               if( $change instanceof RecentChange ) {
-                                       $rcid = $change->mAttribs['rc_id'];
-                               } else {
-                                       // None found
-                                       $rcid = 0;
-                               }
-                       }
-                       // Build the link
-                       if( $rcid ) {
-                               $patrol = ' <span class="patrollink">[' . $sk->makeKnownLinkObj(
-                               $this->mTitle,
-                               wfMsgHtml( 'markaspatrolleddiff' ),
-                                       "action=markpatrolled&rcid={$rcid}"
-                               ) . ']</span>';
-                       } else {
-                               $patrol = '';
-                       }
-               } else {
-                       $patrol = '';
-               }
-
-               $htmldiffarg = $this->htmlDiffArgument();
-               $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousdiff' ),
-                       'diff=prev&oldid='.$this->mOldid.$htmldiffarg, '', '', 'id="differences-prevlink"' );
-               if ( $this->mNewRev->isCurrent() ) {
-                       $nextlink = '&nbsp;';
-               } else {
-                       $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ),
-                               'diff=next&oldid='.$this->mNewid.$htmldiffarg, '', '', 'id="differences-nextlink"' );
-               }
-
-               $oldminor = '';
-               $newminor = '';
-
-               if ($this->mOldRev->mMinorEdit == 1) {
-                       $oldminor = Xml::span( wfMsg( 'minoreditletter'), 'minor' ) . ' ';
-               }
-
-               if ($this->mNewRev->mMinorEdit == 1) {
-                       $newminor = Xml::span( wfMsg( 'minoreditletter'), 'minor' ) . ' ';
-               }
-
-               $rdel = ''; $ldel = '';
-               if( $wgUser->isAllowed( 'deleterevision' ) ) {
-                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
-                       if( !$this->mOldRev->userCan( Revision::DELETED_RESTRICTED ) ) {
-                               // If revision was hidden from sysops
-                               $ldel = wfMsgHtml('rev-delundel');
-                       } else {
-                               $ldel = $sk->makeKnownLinkObj( $revdel,
-                               wfMsgHtml('rev-delundel'),
-                                       'target=' . urlencode( $this->mOldRev->mTitle->getPrefixedDbkey() ) .
-                                       '&oldid=' . urlencode( $this->mOldRev->getId() ) );
-                               // Bolden oversighted content
-                               if( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) )
-                               $ldel = "<strong>$ldel</strong>";
-                       }
-                       $ldel = "&nbsp;&nbsp;&nbsp;<tt>(<small>$ldel</small>)</tt> ";
-                       // We don't currently handle well changing the top revision's settings
-                       if( $this->mNewRev->isCurrent() ) {
-                               // If revision was hidden from sysops
-                               $rdel = wfMsgHtml('rev-delundel');
-                       } else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) {
-                               // If revision was hidden from sysops
-                               $rdel = wfMsgHtml('rev-delundel');
-                       } else {
-                               $rdel = $sk->makeKnownLinkObj( $revdel,
-                               wfMsgHtml('rev-delundel'),
-                                       'target=' . urlencode( $this->mNewRev->mTitle->getPrefixedDbkey() ) .
-                                       '&oldid=' . urlencode( $this->mNewRev->getId() ) );
-                               // Bolden oversighted content
-                               if( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) )
-                               $rdel = "<strong>$rdel</strong>";
-                       }
-                       $rdel = "&nbsp;&nbsp;&nbsp;<tt>(<small>$rdel</small>)</tt> ";
-               }
-
-               $oldHeader = '<div id="mw-diff-otitle1"><strong>'.$this->mOldtitle.'</strong></div>' .
-                       '<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev, true ) . "</div>" .
-                       '<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly, true ) . $ldel . "</div>" .
-                       '<div id="mw-diff-otitle4">' . $prevlink .'</div>';
-               $newHeader = '<div id="mw-diff-ntitle1"><strong>'.$this->mNewtitle.'</strong></div>' .
-                       '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev, true ) . " $rollback</div>" .
-                       '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly, true ) . $rdel . "</div>" .
-                       '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
-
-               if( $wgEnableHtmlDiff && $this->htmldiff) {
-                       $multi = $this->getMultiNotice();
-                       $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'wikicodecomparison' ),
-                       'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=0', '', '', 'id="differences-switchtype"' ).'</div>');
-                       $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
-                       $this->renderHtmlDiff();
-               } else {
-                       if($wgEnableHtmlDiff){
-                               $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'visualcomparison' ),
-                               'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=1', '', '', 'id="differences-switchtype"' ).'</div>');
-                       }
-                       $this->showDiff( $oldHeader, $newHeader );
-                       if( !$diffOnly ) {
-                               $this->renderNewRevision();
-                       }
-               }
-               wfProfileOut( __METHOD__ );
-       }
-
-       /**
-        * Show the new revision of the page.
-        */
-       function renderNewRevision() {
-               global $wgOut;
-               wfProfileIn( __METHOD__ );
-
-               $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
-               #add deleted rev tag if needed
-               if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
-                       $wgOut->addWikiMsg( 'rev-deleted-text-permission' );
-               } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
-                       $wgOut->addWikiMsg( 'rev-deleted-text-view' );
-               }
-
-               if( !$this->mNewRev->isCurrent() ) {
-                       $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
-               }
-
-               $this->loadNewText();
-               if( is_object( $this->mNewRev ) ) {
-                       $wgOut->setRevisionId( $this->mNewRev->getId() );
-               }
-
-               if ($this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage()) {
-                       // Stolen from Article::view --AG 2007-10-11
-
-                       // Give hooks a chance to customise the output
-                       if( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mTitle, $wgOut ) ) ) {
-                               // Wrap the whole lot in a <pre> and don't parse
-                               $m = array();
-                               preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
-                               $wgOut->addHtml( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
-                               $wgOut->addHtml( htmlspecialchars( $this->mNewtext ) );
-                               $wgOut->addHtml( "\n</pre>\n" );
-                       }
-               } else
-               $wgOut->addWikiTextTidy( $this->mNewtext );
-
-               if( !$this->mNewRev->isCurrent() ) {
-                       $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
-               }
-
-               wfProfileOut( __METHOD__ );
-       }
-
-
-       function renderHtmlDiff() {
-               global $wgOut, $wgTitle, $wgParser, $wgDebugComments;
-               wfProfileIn( __METHOD__ );
-
-               $this->showDiffStyle();
-
-               $wgOut->addHTML( '<h2>'.wfMsgHtml( 'visual-comparison' )."</h2>\n" );
-               #add deleted rev tag if needed
-               if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
-                       $wgOut->addWikiMsg( 'rev-deleted-text-permission' );
-               } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
-                       $wgOut->addWikiMsg( 'rev-deleted-text-view' );
-               }
-
-               if( !$this->mNewRev->isCurrent() ) {
-                       $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
-               }
-
-               $this->loadText();
-
-               // Old revision
-               if( is_object( $this->mOldRev ) ) {
-                       $wgOut->setRevisionId( $this->mOldRev->getId() );
-               }
-
-               $popts = $wgOut->parserOptions();
-               $oldTidy = $popts->setTidy( true );
-               $popts->setEditSection( false );
-
-               $parserOutput = $wgParser->parse( $this->mOldtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
-               $popts->setTidy( $oldTidy );
-
-               //only for new?
-               //$wgOut->addParserOutputNoText( $parserOutput );
-               $oldHtml = $parserOutput->getText();
-               wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$oldHtml ) );
-
-               // New revision
-               if( is_object( $this->mNewRev ) ) {
-                       $wgOut->setRevisionId( $this->mNewRev->getId() );
-               }
-
-               $popts = $wgOut->parserOptions();
-               $oldTidy = $popts->setTidy( true );
-
-               $parserOutput = $wgParser->parse( $this->mNewtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
-               $popts->setTidy( $oldTidy );
-
-               $wgOut->addParserOutputNoText( $parserOutput );
-               $newHtml = $parserOutput->getText();
-               wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$newHtml ) );
-
-               unset($parserOutput, $popts);
-
-               $differ = new HTMLDiffer(new DelegatingContentHandler($wgOut));
-               $differ->htmlDiff($oldHtml, $newHtml);
-               if ( $wgDebugComments ) {
-                       $wgOut->addHtml( "\n<!-- HtmlDiff Debug Output:\n" . HTMLDiffer::getDebugOutput() . " End Debug -->" );
-               }
-
-               wfProfileOut( __METHOD__ );
-       }
-
-       /**
-        * Show the first revision of an article. Uses normal diff headers in
-        * contrast to normal "old revision" display style.
-        */
-       function showFirstRevision() {
-               global $wgOut, $wgUser;
-               wfProfileIn( __METHOD__ );
-
-               # Get article text from the DB
-               #
-               if ( ! $this->loadNewText() ) {
-                       $t = $this->mTitle->getPrefixedText();
-                       $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
-                       $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
-                       $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
-                       wfProfileOut( __METHOD__ );
-                       return;
-               }
-               if ( $this->mNewRev->isCurrent() ) {
-                       $wgOut->setArticleFlag( true );
-               }
-
-               # Check if user is allowed to look at this page. If not, bail out.
-               #
-               if ( !( $this->mTitle->userCanRead() ) ) {
-                       $wgOut->loginToUse();
-                       $wgOut->output();
-                       wfProfileOut( __METHOD__ );
-                       exit;
-               }
-
-               # Prepare the header box
-               #
-               $sk = $wgUser->getSkin();
-
-               $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid.$this->htmlDiffArgument(), '', '', 'id="differences-nextlink"' );
-               $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />" .
-               $sk->revUserTools( $this->mNewRev ) . "<br />" .
-               $sk->revComment( $this->mNewRev ) . "<br />" .
-               $nextlink . "</div>\n";
-
-               $wgOut->addHTML( $header );
-
-               $wgOut->setSubtitle( wfMsg( 'difference' ) );
-               $wgOut->setRobotPolicy( 'noindex,nofollow' );
-
-               wfProfileOut( __METHOD__ );
-       }
-
-       function htmlDiffArgument(){
-               global $wgEnableHtmlDiff;
-               if($wgEnableHtmlDiff){
-                       if($this->htmldiff){
-                               return '&htmldiff=1';
-                       }else{
-                               return '&htmldiff=0';
-                       }
-               }else{
-                       return '';
-               }
-       }
-
-       /**
-        * Get the diff text, send it to $wgOut
-        * Returns false if the diff could not be generated, otherwise returns true
-        */
-       function showDiff( $otitle, $ntitle ) {
-               global $wgOut;
-               $diff = $this->getDiff( $otitle, $ntitle );
-               if ( $diff === false ) {
-                       $wgOut->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
-                       return false;
-               } else {
-                       $this->showDiffStyle();
-                       $wgOut->addHTML( $diff );
-                       return true;
-               }
-       }
-
-       /**
-        * Add style sheets and supporting JS for diff display.
-        */
-       function showDiffStyle() {
-               global $wgStylePath, $wgStyleVersion, $wgOut;
-               $wgOut->addStyle( 'common/diff.css' );
-
-               // JS is needed to detect old versions of Mozilla to work around an annoyance bug.
-               $wgOut->addScript( "<script type=\"text/javascript\" src=\"$wgStylePath/common/diff.js?$wgStyleVersion\"></script>" );
-       }
-
-       /**
-        * Get complete diff table, including header
-        *
-        * @param Title $otitle Old title
-        * @param Title $ntitle New title
-        * @return mixed
-        */
-       function getDiff( $otitle, $ntitle ) {
-               $body = $this->getDiffBody();
-               if ( $body === false ) {
-                       return false;
-               } else {
-                       $multi = $this->getMultiNotice();
-                       return $this->addHeader( $body, $otitle, $ntitle, $multi );
-               }
-       }
-
-       /**
-        * Get the diff table body, without header
-        *
-        * @return mixed
-        */
-       function getDiffBody() {
-               global $wgMemc;
-               wfProfileIn( __METHOD__ );
-               // Check if the diff should be hidden from this user
-               if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
-                       return '';
-               } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
-                       return '';
-               }
-               // Cacheable?
-               $key = false;
-               if ( $this->mOldid && $this->mNewid ) {
-                       $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid );
-                       // Try cache
-                       if ( !$this->mRefreshCache ) {
-                               $difftext = $wgMemc->get( $key );
-                               if ( $difftext ) {
-                                       wfIncrStats( 'diff_cache_hit' );
-                                       $difftext = $this->localiseLineNumbers( $difftext );
-                                       $difftext .= "\n<!-- diff cache key $key -->\n";
-                                       wfProfileOut( __METHOD__ );
-                                       return $difftext;
-                               }
-                       } // don't try to load but save the result
-               }
-
-               // Loadtext is permission safe, this just clears out the diff
-               if ( !$this->loadText() ) {
-                       wfProfileOut( __METHOD__ );
-                       return false;
-               }
-
-               $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
-
-               // Save to cache for 7 days
-               if ( $key !== false && $difftext !== false ) {
-                       wfIncrStats( 'diff_cache_miss' );
-                       $wgMemc->set( $key, $difftext, 7*86400 );
-               } else {
-                       wfIncrStats( 'diff_uncacheable' );
-               }
-               // Replace line numbers with the text in the user's language
-               if ( $difftext !== false ) {
-                       $difftext = $this->localiseLineNumbers( $difftext );
-               }
-               wfProfileOut( __METHOD__ );
-               return $difftext;
-       }
-
-       /**
-        * Generate a diff, no caching
-        * $otext and $ntext must be already segmented
-        */
-       function generateDiffBody( $otext, $ntext ) {
-               global $wgExternalDiffEngine, $wgContLang;
-
-               $otext = str_replace( "\r\n", "\n", $otext );
-               $ntext = str_replace( "\r\n", "\n", $ntext );
-
-               if ( $wgExternalDiffEngine == 'wikidiff' ) {
-                       # For historical reasons, external diff engine expects
-                       # input text to be HTML-escaped already
-                       $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
-                       $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
-                       if( !function_exists( 'wikidiff_do_diff' ) ) {
-                               dl('php_wikidiff.so');
-                       }
-                       return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
-                       $this->debug( 'wikidiff1' );
-               }
-
-               if ( $wgExternalDiffEngine == 'wikidiff2' ) {
-                       # Better external diff engine, the 2 may some day be dropped
-                       # This one does the escaping and segmenting itself
-                       if ( !function_exists( 'wikidiff2_do_diff' ) ) {
-                               wfProfileIn( __METHOD__ . "-dl" );
-                               @dl('php_wikidiff2.so');
-                               wfProfileOut( __METHOD__ . "-dl" );
-                       }
-                       if ( function_exists( 'wikidiff2_do_diff' ) ) {
-                               wfProfileIn( 'wikidiff2_do_diff' );
-                               $text = wikidiff2_do_diff( $otext, $ntext, 2 );
-                               $text .= $this->debug( 'wikidiff2' );
-                               wfProfileOut( 'wikidiff2_do_diff' );
-                               return $text;
-                       }
-               }
-               if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
-                       # Diff via the shell
-                       global $wgTmpDirectory;
-                       $tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
-                       $tempName2 = tempnam( $wgTmpDirectory, 'diff_' );
-
-                       $tempFile1 = fopen( $tempName1, "w" );
-                       if ( !$tempFile1 ) {
-                               wfProfileOut( __METHOD__ );
-                               return false;
-                       }
-                       $tempFile2 = fopen( $tempName2, "w" );
-                       if ( !$tempFile2 ) {
-                               wfProfileOut( __METHOD__ );
-                               return false;
-                       }
-                       fwrite( $tempFile1, $otext );
-                       fwrite( $tempFile2, $ntext );
-                       fclose( $tempFile1 );
-                       fclose( $tempFile2 );
-                       $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
-                       wfProfileIn( __METHOD__ . "-shellexec" );
-                       $difftext = wfShellExec( $cmd );
-                       $difftext .= $this->debug( "external $wgExternalDiffEngine" );
-                       wfProfileOut( __METHOD__ . "-shellexec" );
-                       unlink( $tempName1 );
-                       unlink( $tempName2 );
-                       return $difftext;
-               }
-
-               # Native PHP diff
-               $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
-               $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
-               $diffs = new Diff( $ota, $nta );
-               $formatter = new TableDiffFormatter();
-               return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) .
-               $this->debug();
-       }
-
-       /**
-        * Generate a debug comment indicating diff generating time,
-        * server node, and generator backend.
-        */
-       protected function debug( $generator="internal" ) {
-               global $wgShowHostnames, $wgNodeName;
-               $data = array( $generator );
-               if( $wgShowHostnames ) {
-                       $data[] = $wgNodeName;
-               }
-               $data[] = wfTimestamp( TS_DB );
-               return "<!-- diff generator: " .
-               implode( " ",
-               array_map(
-                                       "htmlspecialchars",
-               $data ) ) .
-                       " -->\n";
-       }
-
-       /**
-        * Replace line numbers with the text in the user's language
-        */
-       function localiseLineNumbers( $text ) {
-               return preg_replace_callback( '/<!--LINE (\d+)-->/',
-               array( &$this, 'localiseLineNumbersCb' ), $text );
-       }
-
-       function localiseLineNumbersCb( $matches ) {
-               global $wgLang;
-               return wfMsgExt( 'lineno', array('parseinline'), $wgLang->formatNum( $matches[1] ) );
-       }
-
-
-       /**
-        * If there are revisions between the ones being compared, return a note saying so.
-        */
-       function getMultiNotice() {
-               if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) )
-               return '';
-
-               if( !$this->mOldPage->equals( $this->mNewPage ) ) {
-                       // Comparing two different pages? Count would be meaningless.
-                       return '';
-               }
-
-               $oldid = $this->mOldRev->getId();
-               $newid = $this->mNewRev->getId();
-               if ( $oldid > $newid ) {
-                       $tmp = $oldid; $oldid = $newid; $newid = $tmp;
-               }
-
-               $n = $this->mTitle->countRevisionsBetween( $oldid, $newid );
-               if ( !$n )
-               return '';
-
-               return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n );
-       }
-
-
-       /**
-        * Add the header to a diff body
-        */
-       static function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
-               $header = "
-               <table class='diff'>
-               <col class='diff-marker' />
-               <col class='diff-content' />
-               <col class='diff-marker' />
-               <col class='diff-content' />
-               <tr valign='top'>
-               <td colspan='2' class='diff-otitle'>{$otitle}</td>
-               <td colspan='2' class='diff-ntitle'>{$ntitle}</td>
-               </tr>
-               ";
-
-               if ( $multi != '' )
-               $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>";
-
-               return $header . $diff . "</table>";
-       }
-
-       /**
-        * Use specified text instead of loading from the database
-        */
-       function setText( $oldText, $newText ) {
-               $this->mOldtext = $oldText;
-               $this->mNewtext = $newText;
-               $this->mTextLoaded = 2;
-       }
-
-       /**
-        * Load revision metadata for the specified articles. If newid is 0, then compare
-        * the old article in oldid to the current article; if oldid is 0, then
-        * compare the current article to the immediately previous one (ignoring the
-        * value of newid).
-        *
-        * If oldid is false, leave the corresponding revision object set
-        * to false. This is impossible via ordinary user input, and is provided for
-        * API convenience.
-        */
-       function loadRevisionData() {
-               global $wgLang;
-               if ( $this->mRevisionsLoaded ) {
-                       return true;
-               } else {
-                       // Whether it succeeds or fails, we don't want to try again
-                       $this->mRevisionsLoaded = true;
-               }
-
-               // Load the new revision object
-               $this->mNewRev = $this->mNewid
-               ? Revision::newFromId( $this->mNewid )
-               : Revision::newFromTitle( $this->mTitle );
-               if( !$this->mNewRev instanceof Revision )
-               return false;
-
-               // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
-               $this->mNewid = $this->mNewRev->getId();
-
-               // Check if page is editable
-               $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
-
-               // Set assorted variables
-               $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
-               $this->mNewPage = $this->mNewRev->getTitle();
-               if( $this->mNewRev->isCurrent() ) {
-                       $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
-                       $this->mPagetitle = htmlspecialchars( wfMsg( 'currentrev' ) );
-                       $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' );
-
-                       $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)";
-                       $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
-
-               } else {
-                       $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
-                       $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid );
-                       $this->mPagetitle = wfMsgHTML( 'revisionasof', $timestamp );
-
-                       $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
-                       $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
-               }
-               if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
-                       $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>";
-               } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
-                       $this->mNewtitle = '<span class="history-deleted">'.$this->mNewtitle.'</span>';
-               }
-
-               // Load the old revision object
-               $this->mOldRev = false;
-               if( $this->mOldid ) {
-                       $this->mOldRev = Revision::newFromId( $this->mOldid );
-               } elseif ( $this->mOldid === 0 ) {
-                       $rev = $this->mNewRev->getPrevious();
-                       if( $rev ) {
-                               $this->mOldid = $rev->getId();
-                               $this->mOldRev = $rev;
-                       } else {
-                               // No previous revision; mark to show as first-version only.
-                               $this->mOldid = false;
-                               $this->mOldRev = false;
-                       }
-               }/* elseif ( $this->mOldid === false ) leave mOldRev false; */
-
-               if( is_null( $this->mOldRev ) ) {
-                       return false;
-               }
-
-               if ( $this->mOldRev ) {
-                       $this->mOldPage = $this->mOldRev->getTitle();
-
-                       $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
-                       $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid );
-                       $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid );
-                       $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) );
-
-                       $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
-                       . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
-                       // Add an "undo" link
-                       $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
-                       if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
-                               $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)";
-                       }
-
-                       if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) {
-                               $this->mOldtitle = '<span class="history-deleted">' . $this->mOldPagetitle . '</span>';
-                       } else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
-                               $this->mOldtitle = '<span class="history-deleted">' . $this->mOldtitle . '</span>';
-                       }
-               }
-
-               return true;
-       }
-
-       /**
-        * Load the text of the revisions, as well as revision data.
-        */
-       function loadText() {
-               if ( $this->mTextLoaded == 2 ) {
-                       return true;
-               } else {
-                       // Whether it succeeds or fails, we don't want to try again
-                       $this->mTextLoaded = 2;
-               }
-
-               if ( !$this->loadRevisionData() ) {
-                       return false;
-               }
-               if ( $this->mOldRev ) {
-                       $this->mOldtext = $this->mOldRev->revText();
-                       if ( $this->mOldtext === false ) {
-                               return false;
-                       }
-               }
-               if ( $this->mNewRev ) {
-                       $this->mNewtext = $this->mNewRev->revText();
-                       if ( $this->mNewtext === false ) {
-                               return false;
-                       }
-               }
-               return true;
-       }
-
-       /**
-        * Load the text of the new revision, not the old one
-        */
-       function loadNewText() {
-               if ( $this->mTextLoaded >= 1 ) {
-                       return true;
-               } else {
-                       $this->mTextLoaded = 1;
-               }
-               if ( !$this->loadRevisionData() ) {
-                       return false;
-               }
-               $this->mNewtext = $this->mNewRev->getText();
-               return true;
-       }
-
-
-}
-
-// A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
-//
-// Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
-// You may copy this code freely under the conditions of the GPL.
-//
-
-define('USE_ASSERTS', function_exists('assert'));
-
-/**
- * @todo document
- * @private
- * @ingroup DifferenceEngine
- */
-class _DiffOp {
-       var $type;
-       var $orig;
-       var $closing;
-
-       function reverse() {
-               trigger_error('pure virtual', E_USER_ERROR);
-       }
-
-       function norig() {
-               return $this->orig ? sizeof($this->orig) : 0;
-       }
-
-       function nclosing() {
-               return $this->closing ? sizeof($this->closing) : 0;
-       }
-}
-
-/**
- * @todo document
- * @private
- * @ingroup DifferenceEngine
- */
-class _DiffOp_Copy extends _DiffOp {
-       var $type = 'copy';
-
-       function _DiffOp_Copy ($orig, $closing = false) {
-               if (!is_array($closing))
-               $closing = $orig;
-               $this->orig = $orig;
-               $this->closing = $closing;
-       }
-
-       function reverse() {
-               return new _DiffOp_Copy($this->closing, $this->orig);
-       }
-}
-
-/**
- * @todo document
- * @private
- * @ingroup DifferenceEngine
- */
-class _DiffOp_Delete extends _DiffOp {
-       var $type = 'delete';
-
-       function _DiffOp_Delete ($lines) {
-               $this->orig = $lines;
-               $this->closing = false;
-       }
-
-       function reverse() {
-               return new _DiffOp_Add($this->orig);
-       }
-}
-
-/**
- * @todo document
- * @private
- * @ingroup DifferenceEngine
- */
-class _DiffOp_Add extends _DiffOp {
-       var $type = 'add';
-
-       function _DiffOp_Add ($lines) {
-               $this->closing = $lines;
-               $this->orig = false;
-       }
-
-       function reverse() {
-               return new _DiffOp_Delete($this->closing);
-       }
-}
-
-/**
- * @todo document
- * @private
- * @ingroup DifferenceEngine
- */
-class _DiffOp_Change extends _DiffOp {
-       var $type = 'change';
-
-       function _DiffOp_Change ($orig, $closing) {
-               $this->orig = $orig;
-               $this->closing = $closing;
-       }
-
-       function reverse() {
-               return new _DiffOp_Change($this->closing, $this->orig);
-       }
-}
-
-/**
- * Class used internally by Diff to actually compute the diffs.
- *
- * The algorithm used here is mostly lifted from the perl module
- * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
- *      http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
- *
- * More ideas are taken from:
- *      http://www.ics.uci.edu/~eppstein/161/960229.html
- *
- * Some ideas are (and a bit of code) are from from analyze.c, from GNU
- * diffutils-2.7, which can be found at:
- *      ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
- *
- * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
- * are my own.
- *
- * Line length limits for robustness added by Tim Starling, 2005-08-31
- * Alternative implementation added by Guy Van den Broeck, 2008-07-30
- *
- * @author Geoffrey T. Dairiki, Tim Starling, Guy Van den Broeck
- * @private
- * @ingroup DifferenceEngine
- */
-class _DiffEngine {
-
-       const MAX_XREF_LENGTH =  10000;
-
-       function diff ($from_lines, $to_lines){
-               wfProfileIn( __METHOD__ );
-
-               // Diff and store locally
-               $this->diff_local($from_lines, $to_lines);
-
-               // Merge edits when possible
-               $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
-               $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
-
-               // Compute the edit operations.
-               $n_from = sizeof($from_lines);
-               $n_to = sizeof($to_lines);
-
-               $edits = array();
-               $xi = $yi = 0;
-               while ($xi < $n_from || $yi < $n_to) {
-                       USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
-                       USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
-
-                       // Skip matching "snake".
-                       $copy = array();
-                       while ( $xi < $n_from && $yi < $n_to
-                       && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
-                               $copy[] = $from_lines[$xi++];
-                               ++$yi;
-                       }
-                       if ($copy)
-                       $edits[] = new _DiffOp_Copy($copy);
-
-                       // Find deletes & adds.
-                       $delete = array();
-                       while ($xi < $n_from && $this->xchanged[$xi])
-                       $delete[] = $from_lines[$xi++];
-
-                       $add = array();
-                       while ($yi < $n_to && $this->ychanged[$yi])
-                       $add[] = $to_lines[$yi++];
-
-                       if ($delete && $add)
-                       $edits[] = new _DiffOp_Change($delete, $add);
-                       elseif ($delete)
-                       $edits[] = new _DiffOp_Delete($delete);
-                       elseif ($add)
-                       $edits[] = new _DiffOp_Add($add);
-               }
-               wfProfileOut( __METHOD__ );
-               return $edits;
-       }
-
-       function diff_local ($from_lines, $to_lines) {
-               global $wgExternalDiffEngine;
-               wfProfileIn( __METHOD__);
-
-               if($wgExternalDiffEngine == 'wikidiff3'){
-                       // wikidiff3
-                       $wikidiff3 = new WikiDiff3();
-                       $wikidiff3->diff($from_lines, $to_lines);
-                       $this->xchanged = $wikidiff3->removed;
-                       $this->ychanged = $wikidiff3->added;
-                       unset($wikidiff3);
-               }else{
-                       // old diff
-                       $n_from = sizeof($from_lines);
-                       $n_to = sizeof($to_lines);
-                       $this->xchanged = $this->ychanged = array();
-                       $this->xv = $this->yv = array();
-                       $this->xind = $this->yind = array();
-                       unset($this->seq);
-                       unset($this->in_seq);
-                       unset($this->lcs);
-
-                       // Skip leading common lines.
-                       for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
-                               if ($from_lines[$skip] !== $to_lines[$skip])
-                               break;
-                               $this->xchanged[$skip] = $this->ychanged[$skip] = false;
-                       }
-                       // Skip trailing common lines.
-                       $xi = $n_from; $yi = $n_to;
-                       for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
-                               if ($from_lines[$xi] !== $to_lines[$yi])
-                               break;
-                               $this->xchanged[$xi] = $this->ychanged[$yi] = false;
-                       }
-
-                       // Ignore lines which do not exist in both files.
-                       for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
-                               $xhash[$this->_line_hash($from_lines[$xi])] = 1;
-                       }
-
-                       for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
-                               $line = $to_lines[$yi];
-                               if ( ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) )
-                               continue;
-                               $yhash[$this->_line_hash($line)] = 1;
-                               $this->yv[] = $line;
-                               $this->yind[] = $yi;
-                       }
-                       for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
-                               $line = $from_lines[$xi];
-                               if ( ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) )
-                               continue;
-                               $this->xv[] = $line;
-                               $this->xind[] = $xi;
-                       }
-
-                       // Find the LCS.
-                       $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
-               }
-               wfProfileOut( __METHOD__ );
-       }
-
-       /**
-        * Returns the whole line if it's small enough, or the MD5 hash otherwise
-        */
-       function _line_hash( $line ) {
-               if ( strlen( $line ) > self::MAX_XREF_LENGTH ) {
-                       return md5( $line );
-               } else {
-                       return $line;
-               }
-       }
-
-       /* Divide the Largest Common Subsequence (LCS) of the sequences
-        * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
-        * sized segments.
-        *
-        * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
-        * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
-        * sub sequences.  The first sub-sequence is contained in [X0, X1),
-        * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
-        * that (X0, Y0) == (XOFF, YOFF) and
-        * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
-        *
-        * This function assumes that the first lines of the specified portions
-        * of the two files do not match, and likewise that the last lines do not
-        * match.  The caller must trim matching lines from the beginning and end
-        * of the portions it is going to specify.
-        */
-       function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) {
-               $flip = false;
-
-               if ($xlim - $xoff > $ylim - $yoff) {
-                       // Things seems faster (I'm not sure I understand why)
-                       // when the shortest sequence in X.
-                       $flip = true;
-                       list ($xoff, $xlim, $yoff, $ylim)
-                       = array( $yoff, $ylim, $xoff, $xlim);
-               }
-
-               if ($flip)
-               for ($i = $ylim - 1; $i >= $yoff; $i--)
-               $ymatches[$this->xv[$i]][] = $i;
-               else
-               for ($i = $ylim - 1; $i >= $yoff; $i--)
-               $ymatches[$this->yv[$i]][] = $i;
-
-               $this->lcs = 0;
-               $this->seq[0]= $yoff - 1;
-               $this->in_seq = array();
-               $ymids[0] = array();
-
-               $numer = $xlim - $xoff + $nchunks - 1;
-               $x = $xoff;
-               for ($chunk = 0; $chunk < $nchunks; $chunk++) {
-                       if ($chunk > 0)
-                       for ($i = 0; $i <= $this->lcs; $i++)
-                       $ymids[$i][$chunk-1] = $this->seq[$i];
-
-                       $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
-                       for ( ; $x < $x1; $x++) {
-                               $line = $flip ? $this->yv[$x] : $this->xv[$x];
-                               if (empty($ymatches[$line]))
-                               continue;
-                               $matches = $ymatches[$line];
-                               reset($matches);
-                               while (list ($junk, $y) = each($matches))
-                               if (empty($this->in_seq[$y])) {
-                                       $k = $this->_lcs_pos($y);
-                                       USE_ASSERTS && assert($k > 0);
-                                       $ymids[$k] = $ymids[$k-1];
-                                       break;
-                               }
-                               while (list ( /* $junk */, $y) = each($matches)) {
-                                       if ($y > $this->seq[$k-1]) {
-                                               USE_ASSERTS && assert($y < $this->seq[$k]);
-                                               // Optimization: this is a common case:
-                                               //      next match is just replacing previous match.
-                                               $this->in_seq[$this->seq[$k]] = false;
-                                               $this->seq[$k] = $y;
-                                               $this->in_seq[$y] = 1;
-                                       } else if (empty($this->in_seq[$y])) {
-                                               $k = $this->_lcs_pos($y);
-                                               USE_ASSERTS && assert($k > 0);
-                                               $ymids[$k] = $ymids[$k-1];
-                                       }
-                               }
-                       }
-               }
-
-               $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
-               $ymid = $ymids[$this->lcs];
-               for ($n = 0; $n < $nchunks - 1; $n++) {
-                       $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
-                       $y1 = $ymid[$n] + 1;
-                       $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
-               }
-               $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
-
-               return array($this->lcs, $seps);
-       }
-
-       function _lcs_pos ($ypos) {
-               $end = $this->lcs;
-               if ($end == 0 || $ypos > $this->seq[$end]) {
-                       $this->seq[++$this->lcs] = $ypos;
-                       $this->in_seq[$ypos] = 1;
-                       return $this->lcs;
-               }
-
-               $beg = 1;
-               while ($beg < $end) {
-                       $mid = (int)(($beg + $end) / 2);
-                       if ( $ypos > $this->seq[$mid] )
-                       $beg = $mid + 1;
-                       else
-                       $end = $mid;
-               }
-
-               USE_ASSERTS && assert($ypos != $this->seq[$end]);
-
-               $this->in_seq[$this->seq[$end]] = false;
-               $this->seq[$end] = $ypos;
-               $this->in_seq[$ypos] = 1;
-               return $end;
-       }
-
-       /* Find LCS of two sequences.
-        *
-        * The results are recorded in the vectors $this->{x,y}changed[], by
-        * storing a 1 in the element for each line that is an insertion
-        * or deletion (ie. is not in the LCS).
-        *
-        * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
-        *
-        * Note that XLIM, YLIM are exclusive bounds.
-        * All line numbers are origin-0 and discarded lines are not counted.
-        */
-       function _compareseq ($xoff, $xlim, $yoff, $ylim) {
-               // Slide down the bottom initial diagonal.
-               while ($xoff < $xlim && $yoff < $ylim
-               && $this->xv[$xoff] == $this->yv[$yoff]) {
-                       ++$xoff;
-                       ++$yoff;
-               }
-
-               // Slide up the top initial diagonal.
-               while ($xlim > $xoff && $ylim > $yoff
-               && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
-                       --$xlim;
-                       --$ylim;
-               }
-
-               if ($xoff == $xlim || $yoff == $ylim)
-               $lcs = 0;
-               else {
-                       // This is ad hoc but seems to work well.
-                       //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
-                       //$nchunks = max(2,min(8,(int)$nchunks));
-                       $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
-                       list ($lcs, $seps)
-                       = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
-               }
-
-               if ($lcs == 0) {
-                       // X and Y sequences have no common subsequence:
-                       // mark all changed.
-                       while ($yoff < $ylim)
-                       $this->ychanged[$this->yind[$yoff++]] = 1;
-                       while ($xoff < $xlim)
-                       $this->xchanged[$this->xind[$xoff++]] = 1;
-               } else {
-                       // Use the partitions to split this problem into subproblems.
-                       reset($seps);
-                       $pt1 = $seps[0];
-                       while ($pt2 = next($seps)) {
-                               $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
-                               $pt1 = $pt2;
-                       }
-               }
-       }
-
-       /* Adjust inserts/deletes of identical lines to join changes
-        * as much as possible.
-        *
-        * We do something when a run of changed lines include a
-        * line at one end and has an excluded, identical line at the other.
-        * We are free to choose which identical line is included.
-        * `compareseq' usually chooses the one at the beginning,
-        * but usually it is cleaner to consider the following identical line
-        * to be the "change".
-        *
-        * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
-        */
-       function _shift_boundaries ($lines, &$changed, $other_changed) {
-               wfProfileIn( __METHOD__ );
-               $i = 0;
-               $j = 0;
-
-               USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
-               $len = sizeof($lines);
-               $other_len = sizeof($other_changed);
-
-               while (1) {
-                       /*
-                        * Scan forwards to find beginning of another run of changes.
-                        * Also keep track of the corresponding point in the other file.
-                        *
-                        * Throughout this code, $i and $j are adjusted together so that
-                        * the first $i elements of $changed and the first $j elements
-                        * of $other_changed both contain the same number of zeros
-                        * (unchanged lines).
-                        * Furthermore, $j is always kept so that $j == $other_len or
-                        * $other_changed[$j] == false.
-                        */
-                       while ($j < $other_len && $other_changed[$j])
-                       $j++;
-
-                       while ($i < $len && ! $changed[$i]) {
-                               USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
-                               $i++; $j++;
-                               while ($j < $other_len && $other_changed[$j])
-                               $j++;
-                       }
-
-                       if ($i == $len)
-                       break;
-
-                       $start = $i;
-
-                       // Find the end of this run of changes.
-                       while (++$i < $len && $changed[$i])
-                       continue;
-
-                       do {
-                               /*
-                                * Record the length of this run of changes, so that
-                                * we can later determine whether the run has grown.
-                                */
-                               $runlength = $i - $start;
-
-                               /*
-                                * Move the changed region back, so long as the
-                                * previous unchanged line matches the last changed one.
-                                * This merges with previous changed regions.
-                                */
-                               while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
-                                       $changed[--$start] = 1;
-                                       $changed[--$i] = false;
-                                       while ($start > 0 && $changed[$start - 1])
-                                       $start--;
-                                       USE_ASSERTS && assert('$j > 0');
-                                       while ($other_changed[--$j])
-                                       continue;
-                                       USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
-                               }
-
-                               /*
-                                * Set CORRESPONDING to the end of the changed run, at the last
-                                * point where it corresponds to a changed run in the other file.
-                                * CORRESPONDING == LEN means no such point has been found.
-                                */
-                               $corresponding = $j < $other_len ? $i : $len;
-
-                               /*
-                                * Move the changed region forward, so long as the
-                                * first changed line matches the following unchanged one.
-                                * This merges with following changed regions.
-                                * Do this second, so that if there are no merges,
-                                * the changed region is moved forward as far as possible.
-                                */
-                               while ($i < $len && $lines[$start] == $lines[$i]) {
-                                       $changed[$start++] = false;
-                                       $changed[$i++] = 1;
-                                       while ($i < $len && $changed[$i])
-                                       $i++;
-
-                                       USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
-                                       $j++;
-                                       if ($j < $other_len && $other_changed[$j]) {
-                                               $corresponding = $i;
-                                               while ($j < $other_len && $other_changed[$j])
-                                               $j++;
-                                       }
-                               }
-                       } while ($runlength != $i - $start);
-
-                       /*
-                        * If possible, move the fully-merged run of changes
-                        * back to a corresponding run in the other file.
-                        */
-                       while ($corresponding < $i) {
-                               $changed[--$start] = 1;
-                               $changed[--$i] = 0;
-                               USE_ASSERTS && assert('$j > 0');
-                               while ($other_changed[--$j])
-                               continue;
-                               USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
-                       }
-               }
-               wfProfileOut( __METHOD__ );
-       }
-}
-
-/**
- * Class representing a 'diff' between two sequences of strings.
- * @todo document
- * @private
- * @ingroup DifferenceEngine
- */
-class Diff
-{
-       var $edits;
-
-       /**
-        * Constructor.
-        * Computes diff between sequences of strings.
-        *
-        * @param $from_lines array An array of strings.
-        *                (Typically these are lines from a file.)
-        * @param $to_lines array An array of strings.
-        */
-       function Diff($from_lines, $to_lines) {
-               $eng = new _DiffEngine;
-               $this->edits = $eng->diff($from_lines, $to_lines);
-               //$this->_check($from_lines, $to_lines);
-       }
-
-       /**
-        * Compute reversed Diff.
-        *
-        * SYNOPSIS:
-        *
-        *      $diff = new Diff($lines1, $lines2);
-        *      $rev = $diff->reverse();
-        * @return object A Diff object representing the inverse of the
-        *                                original diff.
-        */
-       function reverse () {
-               $rev = $this;
-               $rev->edits = array();
-               foreach ($this->edits as $edit) {
-                       $rev->edits[] = $edit->reverse();
-               }
-               return $rev;
-       }
-
-       /**
-        * Check for empty diff.
-        *
-        * @return bool True iff two sequences were identical.
-        */
-       function isEmpty () {
-               foreach ($this->edits as $edit) {
-                       if ($edit->type != 'copy')
-                       return false;
-               }
-               return true;
-       }
-
-       /**
-        * Compute the length of the Longest Common Subsequence (LCS).
-        *
-        * This is mostly for diagnostic purposed.
-        *
-        * @return int The length of the LCS.
-        */
-       function lcs () {
-               $lcs = 0;
-               foreach ($this->edits as $edit) {
-                       if ($edit->type == 'copy')
-                       $lcs += sizeof($edit->orig);
-               }
-               return $lcs;
-       }
-
-       /**
-        * Get the original set of lines.
-        *
-        * This reconstructs the $from_lines parameter passed to the
-        * constructor.
-        *
-        * @return array The original sequence of strings.
-        */
-       function orig() {
-               $lines = array();
-
-               foreach ($this->edits as $edit) {
-                       if ($edit->orig)
-                       array_splice($lines, sizeof($lines), 0, $edit->orig);
-               }
-               return $lines;
-       }
-
-       /**
-        * Get the closing set of lines.
-        *
-        * This reconstructs the $to_lines parameter passed to the
-        * constructor.
-        *
-        * @return array The sequence of strings.
-        */
-       function closing() {
-               $lines = array();
-
-               foreach ($this->edits as $edit) {
-                       if ($edit->closing)
-                       array_splice($lines, sizeof($lines), 0, $edit->closing);
-               }
-               return $lines;
-       }
-
-       /**
-        * Check a Diff for validity.
-        *
-        * This is here only for debugging purposes.
-        */
-       function _check ($from_lines, $to_lines) {
-               wfProfileIn( __METHOD__ );
-               if (serialize($from_lines) != serialize($this->orig()))
-               trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
-               if (serialize($to_lines) != serialize($this->closing()))
-               trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
-
-               $rev = $this->reverse();
-               if (serialize($to_lines) != serialize($rev->orig()))
-               trigger_error("Reversed original doesn't match", E_USER_ERROR);
-               if (serialize($from_lines) != serialize($rev->closing()))
-               trigger_error("Reversed closing doesn't match", E_USER_ERROR);
-
-
-               $prevtype = 'none';
-               foreach ($this->edits as $edit) {
-                       if ( $prevtype == $edit->type )
-                       trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
-                       $prevtype = $edit->type;
-               }
-
-               $lcs = $this->lcs();
-               trigger_error('Diff okay: LCS = '.$lcs, E_USER_NOTICE);
-               wfProfileOut( __METHOD__ );
-       }
-}
-
-/**
- * @todo document, bad name.
- * @private
- * @ingroup DifferenceEngine
- */
-class MappedDiff extends Diff
-{
-       /**
-        * Constructor.
-        *
-        * Computes diff between sequences of strings.
-        *
-        * This can be used to compute things like
-        * case-insensitve diffs, or diffs which ignore
-        * changes in white-space.
-        *
-        * @param $from_lines array An array of strings.
-        *      (Typically these are lines from a file.)
-        *
-        * @param $to_lines array An array of strings.
-        *
-        * @param $mapped_from_lines array This array should
-        *      have the same size number of elements as $from_lines.
-        *      The elements in $mapped_from_lines and
-        *      $mapped_to_lines are what is actually compared
-        *      when computing the diff.
-        *
-        * @param $mapped_to_lines array This array should
-        *      have the same number of elements as $to_lines.
-        */
-       function MappedDiff($from_lines, $to_lines,
-       $mapped_from_lines, $mapped_to_lines) {
-               wfProfileIn( __METHOD__ );
-
-               assert(sizeof($from_lines) == sizeof($mapped_from_lines));
-               assert(sizeof($to_lines) == sizeof($mapped_to_lines));
-
-               $this->Diff($mapped_from_lines, $mapped_to_lines);
-
-               $xi = $yi = 0;
-               for ($i = 0; $i < sizeof($this->edits); $i++) {
-                       $orig = &$this->edits[$i]->orig;
-                       if (is_array($orig)) {
-                               $orig = array_slice($from_lines, $xi, sizeof($orig));
-                               $xi += sizeof($orig);
-                       }
-
-                       $closing = &$this->edits[$i]->closing;
-                       if (is_array($closing)) {
-                               $closing = array_slice($to_lines, $yi, sizeof($closing));
-                               $yi += sizeof($closing);
-                       }
-               }
-               wfProfileOut( __METHOD__ );
-       }
-}
-
-/**
- * A class to format Diffs
- *
- * This class formats the diff in classic diff format.
- * It is intended that this class be customized via inheritance,
- * to obtain fancier outputs.
- * @todo document
- * @private
- * @ingroup DifferenceEngine
- */
-class DiffFormatter {
-       /**
-        * Number of leading context "lines" to preserve.
-        *
-        * This should be left at zero for this class, but subclasses
-        * may want to set this to other values.
-        */
-       var $leading_context_lines = 0;
-
-       /**
-        * Number of trailing context "lines" to preserve.
-        *
-        * This should be left at zero for this class, but subclasses
-        * may want to set this to other values.
-        */
-       var $trailing_context_lines = 0;
-
-       /**
-        * Format a diff.
-        *
-        * @param $diff object A Diff object.
-        * @return string The formatted output.
-        */
-       function format($diff) {
-               wfProfileIn( __METHOD__ );
-
-               $xi = $yi = 1;
-               $block = false;
-               $context = array();
-
-               $nlead = $this->leading_context_lines;
-               $ntrail = $this->trailing_context_lines;
-
-               $this->_start_diff();
-
-               foreach ($diff->edits as $edit) {
-                       if ($edit->type == 'copy') {
-                               if (is_array($block)) {
-                                       if (sizeof($edit->orig) <= $nlead + $ntrail) {
-                                               $block[] = $edit;
-                                       }
-                                       else{
-                                               if ($ntrail) {
-                                                       $context = array_slice($edit->orig, 0, $ntrail);
-                                                       $block[] = new _DiffOp_Copy($context);
-                                               }
-                                               $this->_block($x0, $ntrail + $xi - $x0,
-                                               $y0, $ntrail + $yi - $y0,
-                                               $block);
-                                               $block = false;
-                                       }
-                               }
-                               $context = $edit->orig;
-                       }
-                       else {
-                               if (! is_array($block)) {
-                                       $context = array_slice($context, sizeof($context) - $nlead);
-                                       $x0 = $xi - sizeof($context);
-                                       $y0 = $yi - sizeof($context);
-                                       $block = array();
-                                       if ($context)
-                                       $block[] = new _DiffOp_Copy($context);
-                               }
-                               $block[] = $edit;
-                       }
-
-                       if ($edit->orig)
-                       $xi += sizeof($edit->orig);
-                       if ($edit->closing)
-                       $yi += sizeof($edit->closing);
-               }
-
-               if (is_array($block))
-               $this->_block($x0, $xi - $x0,
-               $y0, $yi - $y0,
-               $block);
-
-               $end = $this->_end_diff();
-               wfProfileOut( __METHOD__ );
-               return $end;
-       }
-
-       function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
-               wfProfileIn( __METHOD__ );
-               $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
-               foreach ($edits as $edit) {
-                       if ($edit->type == 'copy')
-                       $this->_context($edit->orig);
-                       elseif ($edit->type == 'add')
-                       $this->_added($edit->closing);
-                       elseif ($edit->type == 'delete')
-                       $this->_deleted($edit->orig);
-                       elseif ($edit->type == 'change')
-                       $this->_changed($edit->orig, $edit->closing);
-                       else
-                       trigger_error('Unknown edit type', E_USER_ERROR);
-               }
-               $this->_end_block();
-               wfProfileOut( __METHOD__ );
-       }
-
-       function _start_diff() {
-               ob_start();
-       }
-
-       function _end_diff() {
-               $val = ob_get_contents();
-               ob_end_clean();
-               return $val;
-       }
-
-       function _block_header($xbeg, $xlen, $ybeg, $ylen) {
-               if ($xlen > 1)
-               $xbeg .= "," . ($xbeg + $xlen - 1);
-               if ($ylen > 1)
-               $ybeg .= "," . ($ybeg + $ylen - 1);
-
-               return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
-       }
-
-       function _start_block($header) {
-               echo $header . "\n";
-       }
-
-       function _end_block() {
-       }
-
-       function _lines($lines, $prefix = ' ') {
-               foreach ($lines as $line)
-               echo "$prefix $line\n";
-       }
-
-       function _context($lines) {
-               $this->_lines($lines);
-       }
-
-       function _added($lines) {
-               $this->_lines($lines, '>');
-       }
-       function _deleted($lines) {
-               $this->_lines($lines, '<');
-       }
-
-       function _changed($orig, $closing) {
-               $this->_deleted($orig);
-               echo "---\n";
-               $this->_added($closing);
-       }
-}
-
-/**
- * A formatter that outputs unified diffs
- * @ingroup DifferenceEngine
- */
-
-class UnifiedDiffFormatter extends DiffFormatter {
-       var $leading_context_lines = 2;
-       var $trailing_context_lines = 2;
-
-       function _added($lines) {
-               $this->_lines($lines, '+');
-       }
-       function _deleted($lines) {
-               $this->_lines($lines, '-');
-       }
-       function _changed($orig, $closing) {
-               $this->_deleted($orig);
-               $this->_added($closing);
-       }
-       function _block_header($xbeg, $xlen, $ybeg, $ylen) {
-               return "@@ -$xbeg,$xlen +$ybeg,$ylen @@";
-       }
-}
-
-/**
- * A pseudo-formatter that just passes along the Diff::$edits array
- * @ingroup DifferenceEngine
- */
-class ArrayDiffFormatter extends DiffFormatter {
-       function format($diff) {
-               $oldline = 1;
-               $newline = 1;
-               $retval = array();
-               foreach($diff->edits as $edit)
-               switch($edit->type) {
-                       case 'add':
-                               foreach($edit->closing as $l) {
-                                       $retval[] = array(
-                                                       'action' => 'add',
-                                                       'new'=> $l,
-                                                       'newline' => $newline++
-                                       );
-                               }
-                               break;
-                       case 'delete':
-                               foreach($edit->orig as $l) {
-                                       $retval[] = array(
-                                                       'action' => 'delete',
-                                                       'old' => $l,
-                                                       'oldline' => $oldline++,
-                                       );
-                               }
-                               break;
-                       case 'change':
-                               foreach($edit->orig as $i => $l) {
-                                       $retval[] = array(
-                                                       'action' => 'change',
-                                                       'old' => $l,
-                                                       'new' => @$edit->closing[$i],
-                                                       'oldline' => $oldline++,
-                                                       'newline' => $newline++,
-                                       );
-                               }
-                               break;
-                       case 'copy':
-                               $oldline += count($edit->orig);
-                               $newline += count($edit->orig);
-               }
-               return $retval;
-       }
-}
-
-/**
- *     Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
- *
- */
-
-define('NBSP', '&#160;'); // iso-8859-x non-breaking space.
-
-/**
- * @todo document
- * @private
- * @ingroup DifferenceEngine
- */
-class _HWLDF_WordAccumulator {
-       function _HWLDF_WordAccumulator () {
-               $this->_lines = array();
-               $this->_line = '';
-               $this->_group = '';
-               $this->_tag = '';
-       }
-
-       function _flushGroup ($new_tag) {
-               if ($this->_group !== '') {
-                       if ($this->_tag == 'ins')
-                       $this->_line .= '<ins class="diffchange diffchange-inline">' .
-                       htmlspecialchars ( $this->_group ) . '</ins>';
-                       elseif ($this->_tag == 'del')
-                       $this->_line .= '<del class="diffchange diffchange-inline">' .
-                       htmlspecialchars ( $this->_group ) . '</del>';
-                       else
-                       $this->_line .= htmlspecialchars ( $this->_group );
-               }
-               $this->_group = '';
-               $this->_tag = $new_tag;
-       }
-
-       function _flushLine ($new_tag) {
-               $this->_flushGroup($new_tag);
-               if ($this->_line != '')
-               array_push ( $this->_lines, $this->_line );
-               else
-               # make empty lines visible by inserting an NBSP
-               array_push ( $this->_lines, NBSP );
-               $this->_line = '';
-       }
-
-       function addWords ($words, $tag = '') {
-               if ($tag != $this->_tag)
-               $this->_flushGroup($tag);
-
-               foreach ($words as $word) {
-                       // new-line should only come as first char of word.
-                       if ($word == '')
-                       continue;
-                       if ($word[0] == "\n") {
-                               $this->_flushLine($tag);
-                               $word = substr($word, 1);
-                       }
-                       assert(!strstr($word, "\n"));
-                       $this->_group .= $word;
-               }
-       }
-
-       function getLines() {
-               $this->_flushLine('~done');
-               return $this->_lines;
-       }
-}
-
-/**
- * @todo document
- * @private
- * @ingroup DifferenceEngine
- */
-class WordLevelDiff extends MappedDiff {
-       const MAX_LINE_LENGTH = 10000;
-
-       function WordLevelDiff ($orig_lines, $closing_lines) {
-               wfProfileIn( __METHOD__ );
-
-               list ($orig_words, $orig_stripped) = $this->_split($orig_lines);
-               list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
-
-               $this->MappedDiff($orig_words, $closing_words,
-               $orig_stripped, $closing_stripped);
-               wfProfileOut( __METHOD__ );
-       }
-
-       function _split($lines) {
-               wfProfileIn( __METHOD__ );
-
-               $words = array();
-               $stripped = array();
-               $first = true;
-               foreach ( $lines as $line ) {
-                       # If the line is too long, just pretend the entire line is one big word
-                       # This prevents resource exhaustion problems
-                       if ( $first ) {
-                               $first = false;
-                       } else {
-                               $words[] = "\n";
-                               $stripped[] = "\n";
-                       }
-                       if ( strlen( $line ) > self::MAX_LINE_LENGTH ) {
-                               $words[] = $line;
-                               $stripped[] = $line;
-                       } else {
-                               $m = array();
-                               if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
-                               $line, $m))
-                               {
-                                       $words = array_merge( $words, $m[0] );
-                                       $stripped = array_merge( $stripped, $m[1] );
-                               }
-                       }
-               }
-               wfProfileOut( __METHOD__ );
-               return array($words, $stripped);
-       }
-
-       function orig () {
-               wfProfileIn( __METHOD__ );
-               $orig = new _HWLDF_WordAccumulator;
-
-               foreach ($this->edits as $edit) {
-                       if ($edit->type == 'copy')
-                       $orig->addWords($edit->orig);
-                       elseif ($edit->orig)
-                       $orig->addWords($edit->orig, 'del');
-               }
-               $lines = $orig->getLines();
-               wfProfileOut( __METHOD__ );
-               return $lines;
-       }
-
-       function closing () {
-               wfProfileIn( __METHOD__ );
-               $closing = new _HWLDF_WordAccumulator;
-
-               foreach ($this->edits as $edit) {
-                       if ($edit->type == 'copy')
-                       $closing->addWords($edit->closing);
-                       elseif ($edit->closing)
-                       $closing->addWords($edit->closing, 'ins');
-               }
-               $lines = $closing->getLines();
-               wfProfileOut( __METHOD__ );
-               return $lines;
-       }
-}
-
-/**
- * Wikipedia Table style diff formatter.
- * @todo document
- * @private
- * @ingroup DifferenceEngine
- */
-class TableDiffFormatter extends DiffFormatter {
-       function TableDiffFormatter() {
-               $this->leading_context_lines = 2;
-               $this->trailing_context_lines = 2;
-       }
-
-       public static function escapeWhiteSpace( $msg ) {
-               $msg = preg_replace( '/^ /m', '&nbsp; ', $msg );
-               $msg = preg_replace( '/ $/m', ' &nbsp;', $msg );
-               $msg = preg_replace( '/  /', '&nbsp; ', $msg );
-               return $msg;
-       }
-
-       function _block_header( $xbeg, $xlen, $ybeg, $ylen ) {
-               $r = '<tr><td colspan="2" class="diff-lineno"><!--LINE '.$xbeg."--></td>\n" .
-                 '<td colspan="2" class="diff-lineno"><!--LINE '.$ybeg."--></td></tr>\n";
-               return $r;
-       }
-
-       function _start_block( $header ) {
-               echo $header;
-       }
-
-       function _end_block() {
-       }
-
-       function _lines( $lines, $prefix=' ', $color='white' ) {
-       }
-
-       # HTML-escape parameter before calling this
-       function addedLine( $line ) {
-               return $this->wrapLine( '+', 'diff-addedline', $line );
-       }
-
-       # HTML-escape parameter before calling this
-       function deletedLine( $line ) {
-               return $this->wrapLine( '-', 'diff-deletedline', $line );
-       }
-
-       # HTML-escape parameter before calling this
-       function contextLine( $line ) {
-               return $this->wrapLine( ' ', 'diff-context', $line );
-       }
-
-       private function wrapLine( $marker, $class, $line ) {
-               if( $line !== '' ) {
-                       // The <div> wrapper is needed for 'overflow: auto' style to scroll properly
-                       $line = Xml::tags( 'div', null, $this->escapeWhiteSpace( $line ) );
-               }
-               return "<td class='diff-marker'>$marker</td><td class='$class'>$line</td>";
-       }
-
-       function emptyLine() {
-               return '<td colspan="2">&nbsp;</td>';
-       }
-
-       function _added( $lines ) {
-               foreach ($lines as $line) {
-                       echo '<tr>' . $this->emptyLine() .
-                       $this->addedLine( '<ins class="diffchange">' .
-                       htmlspecialchars ( $line ) . '</ins>' ) . "</tr>\n";
-               }
-       }
-
-       function _deleted($lines) {
-               foreach ($lines as $line) {
-                       echo '<tr>' . $this->deletedLine( '<del class="diffchange">' .
-                       htmlspecialchars ( $line ) . '</del>' ) .
-                       $this->emptyLine() . "</tr>\n";
-               }
-       }
-
-       function _context( $lines ) {
-               foreach ($lines as $line) {
-                       echo '<tr>' .
-                       $this->contextLine( htmlspecialchars ( $line ) ) .
-                       $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n";
-               }
-       }
-
-       function _changed( $orig, $closing ) {
-               wfProfileIn( __METHOD__ );
-
-               $diff = new WordLevelDiff( $orig, $closing );
-               $del = $diff->orig();
-               $add = $diff->closing();
-
-               # Notice that WordLevelDiff returns HTML-escaped output.
-               # Hence, we will be calling addedLine/deletedLine without HTML-escaping.
-
-               while ( $line = array_shift( $del ) ) {
-                       $aline = array_shift( $add );
-                       echo '<tr>' . $this->deletedLine( $line ) .
-                       $this->addedLine( $aline ) . "</tr>\n";
-               }
-               foreach ($add as $line) {       # If any leftovers
-                       echo '<tr>' . $this->emptyLine() .
-                       $this->addedLine( $line ) . "</tr>\n";
-               }
-               wfProfileOut( __METHOD__ );
-       }
-}
\ No newline at end of file
diff --git a/includes/HTMLDiff.php b/includes/HTMLDiff.php
deleted file mode 100644 (file)
index a5e94aa..0000000
+++ /dev/null
@@ -1,1439 +0,0 @@
-<?php
-/* Copyright (C) 2008 Guy Van den Broeck <guy@guyvdb.eu>
- *
- * This program is free software; you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation; either version 2 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program; if not, write to the Free Software
- * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
- * or see http://www.gnu.org/
- */
-
-/**
- * Any element in the DOM tree of an HTML document.
- */
-class Node {
-
-       public $parent;
-
-       protected $parentTree;
-
-       public $whiteBefore = false;
-
-       public $whiteAfter = false;
-
-       function __construct($parent) {
-               $this->parent = $parent;
-       }
-
-       public function getParentTree() {
-               if (!isset($this->parentTree)) {
-                       if (!is_null($this->parent)) {
-                               $this->parentTree = $this->parent->getParentTree();
-                               $this->parentTree[] = $this->parent;
-                       } else {
-                               $this->parentTree = array();
-                       }
-               }
-               return $this->parentTree;
-       }
-
-       public function getLastCommonParent(Node $other) {
-               $result = new LastCommonParentResult();
-
-               $myParents = $this->getParentTree();
-               $otherParents = $other->getParentTree();
-
-               $i = 1;
-               $isSame = true;
-               $nbMyParents = count($myParents);
-               $nbOtherParents = count($otherParents);
-               while ($isSame && $i < $nbMyParents && $i < $nbOtherParents) {
-                       if (!$myParents[$i]->openingTag === $otherParents[$i]->openingTag) {
-                               $isSame = false;
-                       } else {
-                               // After a while, the index i-1 must be the last common parent
-                               $i++;
-                       }
-               }
-
-               $result->lastCommonParentDepth = $i - 1;
-               $result->parent = $myParents[$i - 1];
-
-               if (!$isSame || $nbMyParents > $nbOtherParents) {
-                       // Not all tags matched, or all tags matched but
-                       // there are tags left in this tree
-                       $result->indexInLastCommonParent = $myParents[$i - 1]->getIndexOf($myParents[$i]);
-                       $result->splittingNeeded = true;
-               } else if ($nbMyParents <= $nbOtherParents) {
-                       $result->indexInLastCommonParent = $myParents[$i - 1]->getIndexOf($this);
-               }
-               return $result;
-       }
-
-       public function setParent($parent) {
-               $this->parent = $parent;
-               unset($this->parentTree);
-       }
-
-       public function inPre() {
-               $tree = $this->getParentTree();
-               foreach ($tree as &$ancestor) {
-                       if ($ancestor->isPre()) {
-                               return true;
-                       }
-               }
-               return false;
-       }
-}
-
-/**
- * Node that can contain other nodes. Represents an HTML tag.
- */
-class TagNode extends Node {
-
-       public $children = array();
-
-       public $qName;
-
-       public $attributes = array();
-
-       public $openingTag;
-
-       function __construct($parent, $qName, /*array*/ $attributes) {
-               parent::__construct($parent);
-               $this->qName = strtolower($qName);
-               foreach($attributes as $key => &$value){
-                       $this->attributes[strtolower($key)] = $value;
-               }
-               return $this->openingTag = Xml::openElement($this->qName, $this->attributes);
-       }
-
-       public function addChildAbsolute(Node $node, $index) {
-               array_splice($this->children, $index, 0, array($node));
-       }
-
-       public function getIndexOf(Node $child) {
-               // don't trust array_search with objects
-               foreach ($this->children as $key => &$value){
-                       if ($value === $child) {
-                               return $key;
-                       }
-               }
-               return null;
-       }
-
-       public function getNbChildren() {
-               return count($this->children);
-       }
-
-       public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) {
-               $nodes = array();
-
-               $allDeleted = false;
-               $somethingDeleted = false;
-               $hasNonDeletedDescendant = false;
-
-               if (empty($this->children)) {
-                       return $nodes;
-               }
-
-               foreach ($this->children as &$child) {
-                       $allDeleted_local = false;
-                       $somethingDeleted_local = false;
-                       $childrenChildren = $child->getMinimalDeletedSet($id, $allDeleted_local, $somethingDeleted_local);
-                       if ($somethingDeleted_local) {
-                               $nodes = array_merge($nodes, $childrenChildren);
-                               $somethingDeleted = true;
-                       }
-                       if (!$allDeleted_local) {
-                               $hasNonDeletedDescendant = true;
-                       }
-               }
-               if (!$hasNonDeletedDescendant) {
-                       $nodes = array($this);
-                       $allDeleted = true;
-               }
-               return $nodes;
-       }
-
-       public function splitUntil(TagNode $parent, Node $split, $includeLeft) {
-               $splitOccured = false;
-               if ($parent !== $this) {
-                       $part1 = new TagNode(null, $this->qName, $this->attributes);
-                       $part2 = new TagNode(null, $this->qName, $this->attributes);
-                       $part1->setParent($this->parent);
-                       $part2->setParent($this->parent);
-
-                       $onSplit = false;
-                       $pastSplit = false;
-                       foreach ($this->children as &$child)
-                       {
-                               if ($child === $split) {
-                                       $onSplit = true;
-                               }
-                               if(!$pastSplit || ($onSplit && $includeLeft)) {
-                                       $child->setParent($part1);
-                                       $part1->children[] = $child;
-                               } else {
-                                       $child->setParent($part2);
-                                       $part2->children[] = $child;
-                               }
-                               if ($onSplit) {
-                                       $onSplit = false;
-                                       $pastSplit = true;
-                               }
-                       }
-                       $myindexinparent = $this->parent->getIndexOf($this);
-                       if (!empty($part1->children)) {
-                               $this->parent->addChildAbsolute($part1, $myindexinparent);
-                       }
-                       if (!empty($part2->children)) {
-                               $this->parent->addChildAbsolute($part2, $myindexinparent);
-                       }
-                       if (!empty($part1->children) && !empty($part2->children)) {
-                               $splitOccured = true;
-                       }
-
-                       $this->parent->removeChild($myindexinparent);
-
-                       if ($includeLeft) {
-                               $this->parent->splitUntil($parent, $part1, $includeLeft);
-                       } else {
-                               $this->parent->splitUntil($parent, $part2, $includeLeft);
-                       }
-               }
-               return $splitOccured;
-
-       }
-
-       private function removeChild($index) {
-               unset($this->children[$index]);
-               $this->children = array_values($this->children);
-       }
-
-       public static $blocks = array('html', 'body','p','blockquote', 'h1',
-               'h2', 'h3', 'h4', 'h5', 'pre', 'div', 'ul', 'ol', 'li', 'table',
-               'tbody', 'tr', 'td', 'th', 'br');
-
-       public function copyTree() {
-               $newThis = new TagNode(null, $this->qName, $this->attributes);
-               $newThis->whiteBefore = $this->whiteBefore;
-               $newThis->whiteAfter = $this->whiteAfter;
-               foreach ($this->children as &$child) {
-                       $newChild = $child->copyTree();
-                       $newChild->setParent($newThis);
-                       $newThis->children[] = $newChild;
-               }
-               return $newThis;
-       }
-
-       public function getMatchRatio(TagNode $other) {
-               $txtComp = new TextOnlyComparator($other);
-               return $txtComp->getMatchRatio(new TextOnlyComparator($this));
-       }
-
-       public function expandWhiteSpace() {
-               $shift = 0;
-               $spaceAdded = false;
-
-               $nbOriginalChildren = $this->getNbChildren();
-               for ($i = 0; $i < $nbOriginalChildren; ++$i) {
-                       $child = $this->children[$i + $shift];
-
-                       if ($child instanceof TagNode) {
-                               if (!$child->isPre()) {
-                                       $child->expandWhiteSpace();
-                               }
-                       }
-                       if (!$spaceAdded && $child->whiteBefore) {
-                               $ws = new WhiteSpaceNode(null, ' ', $child->getLeftMostChild());
-                               $ws->setParent($this);
-                               $this->addChildAbsolute($ws,$i + ($shift++));
-                       }
-                       if ($child->whiteAfter) {
-                               $ws = new WhiteSpaceNode(null, ' ', $child->getRightMostChild());
-                               $ws->setParent($this);
-                               $this->addChildAbsolute($ws,$i + 1 + ($shift++));
-                               $spaceAdded = true;
-                       } else {
-                               $spaceAdded = false;
-                       }
-
-               }
-       }
-
-       public function getLeftMostChild() {
-               if (empty($this->children)) {
-                       return $this;
-               }
-               return $this->children[0]->getLeftMostChild();
-       }
-
-       public function getRightMostChild() {
-               if (empty($this->children)) {
-                       return $this;
-               }
-               return $this->children[$this->getNbChildren() - 1]->getRightMostChild();
-       }
-
-       public function isPre() {
-               return 0 == strcasecmp($this->qName,'pre');
-       }
-
-       public static function toDiffLine(TagNode $node) {
-               return $node->openingTag;
-       }
-}
-
-/**
- * Represents a piece of text in the HTML file.
- */
-class TextNode extends Node {
-
-       public $text;
-
-       public $modification;
-
-       function __construct($parent, $text) {
-               parent::__construct($parent);
-               $this->modification = new Modification(Modification::NONE);
-               $this->text = $text;
-       }
-
-       public function copyTree() {
-               $clone = clone $this;
-               $clone->setParent(null);
-               return $clone;
-       }
-
-       public function getLeftMostChild() {
-               return $this;
-       }
-
-       public function getRightMostChild() {
-               return $this;
-       }
-
-       public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) {
-               if ($this->modification->type == Modification::REMOVED
-                                       && $this->modification->id == $id){
-                       $somethingDeleted = true;
-                       $allDeleted = true;
-                       return array($this);
-               }
-               return array();
-       }
-
-       public function isSameText($other) {
-               if (is_null($other) || ! $other instanceof TextNode) {
-                       return false;
-               }
-               return str_replace('\n', ' ',$this->text) === str_replace('\n', ' ',$other->text);
-       }
-
-       public static function toDiffLine(TextNode $node) {
-               return str_replace('\n', ' ',$node->text);
-       }
-}
-
-class WhiteSpaceNode extends TextNode {
-
-       function __construct($parent, $s, Node $like = null) {
-               parent::__construct($parent, $s);
-               if(!is_null($like) && $like instanceof TextNode) {
-                       $newModification = clone $like->modification;
-                       $newModification->firstOfID = false;
-                       $this->modification = $newModification;
-               }
-       }
-}
-
-/**
- * Represents the root of a HTML document.
- */
-class BodyNode extends TagNode {
-
-       function __construct() {
-               parent::__construct(null, 'body', array());
-       }
-
-       public function copyTree() {
-               $newThis = new BodyNode();
-               foreach ($this->children as &$child) {
-                       $newChild = $child->copyTree();
-                       $newChild->setParent($newThis);
-                       $newThis->children[] = $newChild;
-               }
-               return $newThis;
-       }
-
-       public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) {
-               $nodes = array();
-               foreach ($this->children as &$child) {
-                       $childrenChildren = $child->getMinimalDeletedSet($id,
-                                               $allDeleted, $somethingDeleted);
-                       $nodes = array_merge($nodes, $childrenChildren);
-               }
-               return $nodes;
-       }
-
-}
-
-/**
- * Represents an image in HTML. Even though images do not contain any text they
- * are independent visible objects on the page. They are logically a TextNode.
- */
-class ImageNode extends TextNode {
-
-       public $attributes;
-
-       function __construct(TagNode $parent, /*array*/ $attrs) {
-               if(!array_key_exists('src', $attrs)) {
-                       HTMLDiffer::diffDebug( "Image without a source\n" );
-                       parent::__construct($parent, '<img></img>');
-               }else{
-                       parent::__construct($parent, '<img>' . strtolower($attrs['src']) . '</img>');
-               }
-               $this->attributes = $attrs;
-       }
-
-       public function isSameText($other) {
-               if (is_null($other) || ! $other instanceof ImageNode) {
-                       return false;
-               }
-               return $this->text === $other->text;
-       }
-
-}
-
-class DummyNode extends Node {
-
-       function __construct() {
-               // no op
-       }
-
-}
-
-/**
- * When detecting the last common parent of two nodes, all results are stored as
- * a LastCommonParentResult.
- */
-class LastCommonParentResult {
-
-       // Parent
-       public $parent;
-
-       // Splitting
-       public $splittingNeeded = false;
-
-       // Depth
-       public $lastCommonParentDepth = -1;
-
-       // Index
-       public $indexInLastCommonParent = -1;
-}
-
-class Modification{
-
-       const NONE = 1;
-       const REMOVED = 2;
-       const ADDED = 4;
-       const CHANGED = 8;
-
-       public $type;
-
-       public $id = -1;
-
-       public $firstOfID = false;
-
-       public $changes;
-
-       function __construct($type) {
-               $this->type = $type;
-       }
-
-       public static function typeToString($type) {
-               switch($type) {
-                       case self::NONE: return 'none';
-                       case self::REMOVED: return 'removed';
-                       case self::ADDED: return 'added';
-                       case self::CHANGED: return 'changed';
-               }
-       }
-}
-
-class DomTreeBuilder {
-
-       public $textNodes = array();
-
-       public $bodyNode;
-
-       private $currentParent;
-
-       private $newWord = '';
-
-       protected $bodyStarted = false;
-
-       protected $bodyEnded = false;
-
-       private $whiteSpaceBeforeThis = false;
-
-       private $lastSibling;
-
-       private $notInPre = true;
-
-       function __construct() {
-               $this->bodyNode = $this->currentParent = new BodyNode();
-               $this->lastSibling = new DummyNode();
-       }
-
-       /**
-        * Must be called manually
-        */
-       public function endDocument() {
-               $this->endWord();
-               HTMLDiffer::diffDebug( count($this->textNodes) . " text nodes in document.\n" );
-       }
-
-       public function startElement($parser, $name, /*array*/ $attributes) {
-               if (strcasecmp($name, 'body') != 0) {
-                       HTMLDiffer::diffDebug( "Starting $name node.\n" );
-                       $this->endWord();
-
-                       $newNode = new TagNode($this->currentParent, $name, $attributes);
-                       $this->currentParent->children[] = $newNode;
-                       $this->currentParent = $newNode;
-                       $this->lastSibling = new DummyNode();
-                       if ($this->whiteSpaceBeforeThis && !in_array(strtolower($this->currentParent->qName),TagNode::$blocks)) {
-                               $this->currentParent->whiteBefore = true;
-                       }
-                       $this->whiteSpaceBeforeThis = false;
-                       if(strcasecmp($name, 'pre') == 0) {
-                               $this->notInPre = false;
-                       }
-               }
-       }
-
-       public function endElement($parser, $name) {
-               if(strcasecmp($name, 'body') != 0) {
-                       HTMLDiffer::diffDebug( "Ending $name node.\n");
-                       if (0 == strcasecmp($name,'img')) {
-                               // Insert a dummy leaf for the image
-                               $img = new ImageNode($this->currentParent, $this->currentParent->attributes);
-                               $this->currentParent->children[] = $img;
-                               $img->whiteBefore = $this->whiteSpaceBeforeThis;
-                               $this->lastSibling = $img;
-                               $this->textNodes[] = $img;
-                       }
-                       $this->endWord();
-                       if (!in_array(strtolower($this->currentParent->qName),TagNode::$blocks)) {
-                               $this->lastSibling = $this->currentParent;
-                       } else {
-                               $this->lastSibling = new DummyNode();
-                       }
-                       $this->currentParent = $this->currentParent->parent;
-                       $this->whiteSpaceBeforeThis = false;
-                       if (!$this->notInPre && strcasecmp($name, 'pre') == 0) {
-                               $this->notInPre = true;
-                       }
-               } else {
-                       $this->endDocument();
-               }
-       }
-
-       const regex = '/([\s\.\,\"\\\'\(\)\?\:\;\!\{\}\-\+\*\=\_\[\]\&\|\$]{1})/';
-       const whitespace = '/^[\s]{1}$/';
-       const delimiter = '/^[\s\.\,\"\\\'\(\)\?\:\;\!\{\}\-\+\*\=\_\[\]\&\|\$]{1}$/';
-
-       public function characters($parser, $data) {
-               $matches = preg_split(self::regex, $data, -1, PREG_SPLIT_DELIM_CAPTURE);
-
-               foreach($matches as &$word) {
-                       if (preg_match(self::whitespace, $word) && $this->notInPre) {
-                               $this->endWord();
-                               $this->lastSibling->whiteAfter = true;
-                               $this->whiteSpaceBeforeThis = true;
-                       } else if (preg_match(self::delimiter, $word)) {
-                               $this->endWord();
-                               $textNode = new TextNode($this->currentParent, $word);
-                               $this->currentParent->children[] = $textNode;
-                               $textNode->whiteBefore = $this->whiteSpaceBeforeThis;
-                               $this->whiteSpaceBeforeThis = false;
-                               $this->lastSibling = $textNode;
-                               $this->textNodes[] = $textNode;
-                       } else {
-                               $this->newWord .= $word;
-                       }
-               }
-       }
-
-       private function endWord() {
-               if ($this->newWord !== '') {
-                       $node = new TextNode($this->currentParent, $this->newWord);
-                       $this->currentParent->children[] = $node;
-                       $node->whiteBefore = $this->whiteSpaceBeforeThis;
-                       $this->whiteSpaceBeforeThis = false;
-                       $this->lastSibling = $node;
-                       $this->textNodes[] = $node;
-                       $this->newWord = "";
-               }
-       }
-
-       public function getDiffLines() {
-               return array_map(array('TextNode','toDiffLine'), $this->textNodes);
-       }
-}
-
-class TextNodeDiffer {
-
-       private $textNodes;
-       public $bodyNode;
-
-       private $oldTextNodes;
-       private $oldBodyNode;
-
-       private $newID = 0;
-
-       private $changedID = 0;
-
-       private $changedIDUsed = false;
-
-       // used to remove the whitespace between a red and green block
-       private $whiteAfterLastChangedPart = false;
-
-       private $deletedID = 0;
-
-       function __construct(DomTreeBuilder $tree, DomTreeBuilder $oldTree) {
-               $this->textNodes = $tree->textNodes;
-               $this->bodyNode = $tree->bodyNode;
-               $this->oldTextNodes = $oldTree->textNodes;
-               $this->oldBodyNode = $oldTree->bodyNode;
-       }
-
-       public function markAsNew($start, $end) {
-               if ($end <= $start) {
-                       return;
-               }
-
-               if ($this->whiteAfterLastChangedPart) {
-                       $this->textNodes[$start]->whiteBefore = false;
-               }
-
-               for ($i = $start; $i < $end; ++$i) {
-                       $mod = new Modification(Modification::ADDED);
-                       $mod->id = $this->newID;
-                       $this->textNodes[$i]->modification = $mod;
-               }
-               if ($start < $end) {
-                       $this->textNodes[$start]->modification->firstOfID = true;
-               }
-               ++$this->newID;
-       }
-
-       public function handlePossibleChangedPart($leftstart, $leftend, $rightstart, $rightend) {
-               $i = $rightstart;
-               $j = $leftstart;
-
-               if ($this->changedIDUsed) {
-                       ++$this->changedID;
-                       $this->changedIDUsed = false;
-               }
-
-               $changes;
-               while ($i < $rightend) {
-                       $acthis = new AncestorComparator($this->textNodes[$i]->getParentTree());
-                       $acother = new AncestorComparator($this->oldTextNodes[$j]->getParentTree());
-                       $result = $acthis->getResult($acother);
-                       unset($acthis, $acother);
-
-                       if ($result->changed) {
-                               $mod = new Modification(Modification::CHANGED);
-
-                               if (!$this->changedIDUsed) {
-                                       $mod->firstOfID = true;
-                               } else if (!is_null($result->changes) && $result->changes !== $this->changes) {
-                                       ++$this->changedID;
-                                       $mod->firstOfID = true;
-                               }
-
-                               $mod->changes = $result->changes;
-                               $mod->id = $this->changedID;
-
-                               $this->textNodes[$i]->modification = $mod;
-                               $this->changes = $result->changes;
-                               $this->changedIDUsed = true;
-                       } else if ($this->changedIDUsed) {
-                               ++$this->changedID;
-                               $this->changedIDUsed = false;
-                       }
-                       ++$i;
-                       ++$j;
-               }
-       }
-
-       public function markAsDeleted($start, $end, $before) {
-
-               if ($end <= $start) {
-                       return;
-               }
-
-               if ($before > 0 && $this->textNodes[$before - 1]->whiteAfter) {
-                       $this->whiteAfterLastChangedPart = true;
-               } else {
-                       $this->whiteAfterLastChangedPart = false;
-               }
-
-               for ($i = $start; $i < $end; ++$i) {
-                       $mod = new Modification(Modification::REMOVED);
-                       $mod->id = $this->deletedID;
-
-                       // oldTextNodes is used here because we're going to move its deleted
-                       // elements to this tree!
-                       $this->oldTextNodes[$i]->modification = $mod;
-               }
-               $this->oldTextNodes[$start]->modification->firstOfID = true;
-
-               $root = $this->oldTextNodes[$start]->getLastCommonParent($this->oldTextNodes[$end-1])->parent;
-
-               $junk1 = $junk2 = null;
-               $deletedNodes = $root->getMinimalDeletedSet($this->deletedID, $junk1, $junk2);
-
-               HTMLDiffer::diffDebug( "Minimal set of deleted nodes of size " . count($deletedNodes) . "\n" );
-
-               // Set prevLeaf to the leaf after which the old HTML needs to be
-               // inserted
-               if ($before > 0) {
-                       $prevLeaf = $this->textNodes[$before - 1];
-               }
-               // Set nextLeaf to the leaf before which the old HTML needs to be
-               // inserted
-               if ($before < count($this->textNodes)) {
-                       $nextLeaf = $this->textNodes[$before];
-               }
-
-               while (count($deletedNodes) > 0) {
-                       if (isset($prevLeaf)) {
-                               $prevResult = $prevLeaf->getLastCommonParent($deletedNodes[0]);
-                       } else {
-                               $prevResult = new LastCommonParentResult();
-                               $prevResult->parent = $this->bodyNode;
-                               $prevResult->indexInLastCommonParent = 0;
-                       }
-                       if (isset($nextleaf)) {
-                               $nextResult = $nextLeaf->getLastCommonParent($deletedNodes[count($deletedNodes) - 1]);
-                       } else {
-                               $nextResult = new LastCommonParentResult();
-                               $nextResult->parent = $this->bodyNode;
-                               $nextResult->indexInLastCommonParent = $this->bodyNode->getNbChildren();
-                       }
-
-                       if ($prevResult->lastCommonParentDepth == $nextResult->lastCommonParentDepth) {
-                               // We need some metric to choose which way to add-...
-                               if ($deletedNodes[0]->parent === $deletedNodes[count($deletedNodes) - 1]->parent
-                                               && $prevResult->parent === $nextResult->parent) {
-                                       // The difference is not in the parent
-                                       $prevResult->lastCommonParentDepth = $prevResult->lastCommonParentDepth + 1;
-                               } else {
-                                       // The difference is in the parent, so compare them
-                                       // now THIS is tricky
-                                       $distancePrev = $deletedNodes[0]->parent->getMatchRatio($prevResult->parent);
-                                       $distanceNext = $deletedNodes[count($deletedNodes) - 1]->parent->getMatchRatio($nextResult->parent);
-
-                                       if ($distancePrev <= $distanceNext) {
-                                               $prevResult->lastCommonParentDepth = $prevResult->lastCommonParentDepth + 1;
-                                       } else {
-                                               $nextResult->lastCommonParentDepth = $nextResult->lastCommonParentDepth + 1;
-                                       }
-                               }
-
-                       }
-
-                       if ($prevResult->lastCommonParentDepth > $nextResult->lastCommonParentDepth) {
-                               // Inserting at the front
-                               if ($prevResult->splittingNeeded) {
-                                       $prevLeaf->parent->splitUntil($prevResult->parent, $prevLeaf, true);
-                               }
-                               $prevLeaf = $deletedNodes[0]->copyTree();
-                               unset($deletedNodes[0]);
-                               $deletedNodes = array_values($deletedNodes);
-                               $prevLeaf->setParent($prevResult->parent);
-                               $prevResult->parent->addChildAbsolute($prevLeaf,$prevResult->indexInLastCommonParent + 1);
-                       } else if ($prevResult->lastCommonParentDepth < $nextResult->lastCommonParentDepth) {
-                               // Inserting at the back
-                               if ($nextResult->splittingNeeded) {
-                                       $splitOccured = $nextLeaf->parent->splitUntil($nextResult->parent, $nextLeaf, false);
-                                       if ($splitOccured) {
-                                               // The place where to insert is shifted one place to the
-                                               // right
-                                               $nextResult->indexInLastCommonParent = $nextResult->indexInLastCommonParent + 1;
-                                       }
-                               }
-                               $nextLeaf = $deletedNodes[count(deletedNodes) - 1]->copyTree();
-                               unset($deletedNodes[count(deletedNodes) - 1]);
-                               $deletedNodes = array_values($deletedNodes);
-                               $nextLeaf->setParent($nextResult->parent);
-                               $nextResult->parent->addChildAbsolute($nextLeaf,$nextResult->indexInLastCommonParent);
-                       }
-               }
-               ++$this->deletedID;
-       }
-
-       public function expandWhiteSpace() {
-               $this->bodyNode->expandWhiteSpace();
-       }
-
-       public function lengthNew(){
-               return count($this->textNodes);
-       }
-
-       public function lengthOld(){
-               return count($this->oldTextNodes);
-       }
-}
-
-class HTMLDiffer {
-
-       private $output;
-       private static $debug = '';
-
-       function __construct($output) {
-               $this->output = $output;
-       }
-
-       function htmlDiff($from, $to) {
-               wfProfileIn( __METHOD__ );
-               // Create an XML parser
-               $xml_parser = xml_parser_create('');
-
-               $domfrom = new DomTreeBuilder();
-
-               // Set the functions to handle opening and closing tags
-               xml_set_element_handler($xml_parser, array($domfrom, "startElement"), array($domfrom, "endElement"));
-
-               // Set the function to handle blocks of character data
-               xml_set_character_data_handler($xml_parser, array($domfrom, "characters"));
-
-               HTMLDiffer::diffDebug( "Parsing " . strlen($from) . " characters worth of HTML\n" );
-               if (!xml_parse($xml_parser, '<?xml version="1.0" encoding="UTF-8"?>'.Sanitizer::hackDocType().'<body>', false)
-                                       || !xml_parse($xml_parser, $from, false)
-                                       || !xml_parse($xml_parser, '</body>', true)){
-                       $error = xml_error_string(xml_get_error_code($xml_parser));
-                       $line = xml_get_current_line_number($xml_parser);
-                       HTMLDiffer::diffDebug( "XML error: $error at line $line\n" );
-               }
-               xml_parser_free($xml_parser);
-               unset($from);
-
-               $xml_parser = xml_parser_create('');
-
-               $domto = new DomTreeBuilder();
-
-               // Set the functions to handle opening and closing tags
-               xml_set_element_handler($xml_parser, array($domto, "startElement"), array($domto, "endElement"));
-
-               // Set the function to handle blocks of character data
-               xml_set_character_data_handler($xml_parser, array($domto, "characters"));
-
-               HTMLDiffer::diffDebug( "Parsing " . strlen($to) . " characters worth of HTML\n" );
-               if (!xml_parse($xml_parser, '<?xml version="1.0" encoding="UTF-8"?>'.Sanitizer::hackDocType().'<body>', false)
-               || !xml_parse($xml_parser, $to, false)
-               || !xml_parse($xml_parser, '</body>', true)){
-                       $error = xml_error_string(xml_get_error_code($xml_parser));
-                       $line = xml_get_current_line_number($xml_parser);
-                       HTMLDiffer::diffDebug( "XML error: $error at line $line\n" );
-               }
-               xml_parser_free($xml_parser);
-               unset($to);
-
-               $diffengine = new WikiDiff3();
-               $differences = $this->preProcess($diffengine->diff_range($domfrom->getDiffLines(), $domto->getDiffLines()));
-               unset($xml_parser, $diffengine);
-
-               $domdiffer = new TextNodeDiffer($domto, $domfrom);
-
-               $currentIndexLeft = 0;
-               $currentIndexRight = 0;
-               foreach ($differences as &$d) {
-                       if ($d->leftstart > $currentIndexLeft) {
-                               $domdiffer->handlePossibleChangedPart($currentIndexLeft, $d->leftstart,
-                                       $currentIndexRight, $d->rightstart);
-                       }
-                       if ($d->leftlength > 0) {
-                               $domdiffer->markAsDeleted($d->leftstart, $d->leftend, $d->rightstart);
-                       }
-                       $domdiffer->markAsNew($d->rightstart, $d->rightend);
-
-                       $currentIndexLeft = $d->leftend;
-                       $currentIndexRight = $d->rightend;
-               }
-               $oldLength = $domdiffer->lengthOld();
-               if ($currentIndexLeft < $oldLength) {
-                       $domdiffer->handlePossibleChangedPart($currentIndexLeft, $oldLength, $currentIndexRight, $domdiffer->lengthNew());
-               }
-               $domdiffer->expandWhiteSpace();
-               $output = new HTMLOutput('htmldiff', $this->output);
-               $output->parse($domdiffer->bodyNode);
-               wfProfileOut( __METHOD__ );
-       }
-
-       private function preProcess(/*array*/ $differences) {
-               $newRanges = array();
-
-               $nbDifferences = count($differences);
-               for ($i = 0; $i < $nbDifferences; ++$i) {
-                       $leftStart = $differences[$i]->leftstart;
-                       $leftEnd = $differences[$i]->leftend;
-                       $rightStart = $differences[$i]->rightstart;
-                       $rightEnd = $differences[$i]->rightend;
-
-                       $leftLength = $leftEnd - $leftStart;
-                       $rightLength = $rightEnd - $rightStart;
-
-                       while ($i + 1 < $nbDifferences && self::score($leftLength,
-                                               $differences[$i + 1]->leftlength,
-                                               $rightLength,
-                                               $differences[$i + 1]->rightlength)
-                                       > ($differences[$i + 1]->leftstart - $leftEnd)) {
-                               $leftEnd = $differences[$i + 1]->leftend;
-                               $rightEnd = $differences[$i + 1]->rightend;
-                               $leftLength = $leftEnd - $leftStart;
-                               $rightLength = $rightEnd - $rightStart;
-                               ++$i;
-                       }
-                       $newRanges[] = new RangeDifference($leftStart, $leftEnd, $rightStart, $rightEnd);
-               }
-               return $newRanges;
-       }
-
-       /**
-        * Heuristic to merge differences for readability.
-        */
-       public static function score($ll, $nll, $rl, $nrl) {
-               if (($ll == 0 && $nll == 0)
-                               || ($rl == 0 && $nrl == 0)) {
-                       return 0;
-               }
-               $numbers = array($ll, $nll, $rl, $nrl);
-               $d = 0;
-               foreach ($numbers as &$number) {
-                       while ($number > 3) {
-                               $d += 3;
-                               $number -= 3;
-                               $number *= 0.5;
-                       }
-                       $d += $number;
-
-               }
-               return $d / (1.5 * count($numbers));
-       }
-
-       /**
-        * Add to debug output
-        * @param string $str Debug output
-        */
-       public static function diffDebug( $str ) {
-               self :: $debug .= $str;
-       }
-       
-       /**
-        * Get debug output
-        * @return string
-        */
-       public static function getDebugOutput() {
-               return self :: $debug;
-       }
-
-}
-
-class TextOnlyComparator {
-
-       public $leafs = array();
-
-       function _construct(TagNode $tree) {
-               $this->addRecursive($tree);
-               $this->leafs = array_map(array('TextNode','toDiffLine'), $this->leafs);
-       }
-
-       private function addRecursive(TagNode $tree) {
-               foreach ($tree->children as &$child) {
-                       if ($child instanceof TagNode) {
-                               $this->addRecursive($child);
-                       } else if ($child instanceof TextNode) {
-                               $this->leafs[] = $node;
-                       }
-               }
-       }
-
-       public function getMatchRatio(TextOnlyComparator $other) {
-               $nbOthers = count($other->leafs);
-               $nbThis = count($this->leafs);
-               if($nbOthers == 0 || $nbThis == 0){
-                       return -log(0);
-               }
-
-               $diffengine = new WikiDiff3(25000, 1.35);
-               $diffengine->diff($this->leafs, $other->leafs);
-
-               $lcsLength = $diffengine->getLcsLength();
-
-               $distanceThis = $nbThis-$lcsLength;
-
-               return (2.0 - $lcsLength/$nbOthers - $lcsLength/$nbThis) / 2.0;
-       }
-}
-
-class AncestorComparatorResult {
-
-       public $changed = false;
-
-       public $changes = "";
-}
-
-/**
- * A comparator used when calculating the difference in ancestry of two Nodes.
- */
-class AncestorComparator {
-
-       public $ancestors;
-       public $ancestorsText;
-
-       function __construct(/*array*/ $ancestors) {
-               $this->ancestors = $ancestors;
-               $this->ancestorsText = array_map(array('TagNode','toDiffLine'), $ancestors);
-       }
-
-       public $compareTxt = "";
-
-       public function getResult(AncestorComparator $other) {
-               $result = new AncestorComparatorResult();
-
-               $diffengine = new WikiDiff3(10000, 1.35);
-               $differences = $diffengine->diff_range($other->ancestorsText,$this->ancestorsText);
-
-               if (count($differences) == 0){
-                       return $result;
-               }
-               $changeTxt = new ChangeTextGenerator($this, $other);
-
-               $result->changed = true;
-               $result->changes = $changeTxt->getChanged($differences)->toString();
-
-               return $result;
-       }
-}
-
-class ChangeTextGenerator {
-
-       private $ancestorComparator;
-       private $other;
-
-       private $factory;
-
-       function __construct(AncestorComparator $ancestorComparator, AncestorComparator $other) {
-               $this->ancestorComparator = $ancestorComparator;
-               $this->other = $other;
-               $this->factory = new TagToStringFactory();
-       }
-
-       public function getChanged(/*array*/ $differences) {
-               $txt = new ChangeText;
-               $rootlistopened = false;
-               if (count($differences) > 1) {
-                       $txt->addHtml('<ul class="changelist">');
-                       $rootlistopened = true;
-               }
-               $nbDifferences = count($differences);
-               for ($j = 0; $j < $nbDifferences; ++$j) {
-                       $d = $differences[$j];
-                       $lvl1listopened = false;
-                       if ($rootlistopened) {
-                               $txt->addHtml('<li>');
-                       }
-                       if ($d->leftlength + $d->rightlength > 1) {
-                               $txt->addHtml('<ul class="changelist">');
-                               $lvl1listopened = true;
-                       }
-                       // left are the old ones
-                       for ($i = $d->leftstart; $i < $d->leftend; ++$i) {
-                               if ($lvl1listopened){
-                                       $txt->addHtml('<li>');
-                               }
-                               // add a bullet for a old tag
-                               $this->addTagOld($txt, $this->other->ancestors[$i]);
-                               if ($lvl1listopened){
-                                       $txt->addHtml('</li>');
-                               }
-                       }
-                       // right are the new ones
-                       for ($i = $d->rightstart; $i < $d->rightend; ++$i) {
-                               if ($lvl1listopened){
-                                       $txt->addHtml('<li>');
-                               }
-                               // add a bullet for a new tag
-                               $this->addTagNew($txt, $this->ancestorComparator->ancestors[$i]);
-
-                               if ($lvl1listopened){
-                                       $txt->addHtml('</li>');
-                               }
-                       }
-                       if ($lvl1listopened) {
-                               $txt->addHtml('</ul>');
-                       }
-                       if ($rootlistopened) {
-                               $txt->addHtml('</li>');
-                       }
-               }
-               if ($rootlistopened) {
-                       $txt->addHtml('</ul>');
-               }
-               return $txt;
-       }
-
-       private function addTagOld(ChangeText $txt, TagNode $ancestor) {
-               $this->factory->create($ancestor)->getRemovedDescription($txt);
-       }
-
-       private function addTagNew(ChangeText $txt, TagNode $ancestor) {
-               $this->factory->create($ancestor)->getAddedDescription($txt);
-       }
-}
-
-class ChangeText {
-
-       private $txt = "";
-
-       public function addHtml($s) {
-               $this->txt .= $s;
-       }
-
-       public function toString() {
-               return $this->txt;
-       }
-}
-
-class TagToStringFactory {
-
-       private static $containerTags = array('html', 'body', 'p', 'blockquote',
-               'h1', 'h2', 'h3', 'h4', 'h5', 'pre', 'div', 'ul', 'ol', 'li',
-               'table', 'tbody', 'tr', 'td', 'th', 'br', 'hr', 'code', 'dl',
-               'dt', 'dd', 'input', 'form', 'img', 'span', 'a');
-
-       private static $styleTags = array('i', 'b', 'strong', 'em', 'font',
-               'big', 'del', 'tt', 'sub', 'sup', 'strike');
-
-       const MOVED = 1;
-       const STYLE = 2;
-       const UNKNOWN = 4;
-
-       public function create(TagNode $node) {
-               $sem = $this->getChangeSemantic($node->qName);
-               if (strcasecmp($node->qName,'a') == 0) {
-                       return new AnchorToString($node, $sem);
-               }
-               if (strcasecmp($node->qName,'img') == 0) {
-                       return new NoContentTagToString($node, $sem);
-               }
-               return new TagToString($node, $sem);
-       }
-
-       protected function getChangeSemantic($qname) {
-               if (in_array(strtolower($qname),self::$containerTags)) {
-                       return self::MOVED;
-               }
-               if (in_array(strtolower($qname),self::$styleTags)) {
-                       return self::STYLE;
-               }
-               return self::UNKNOWN;
-       }
-}
-
-class TagToString {
-
-       protected $node;
-
-       protected $sem;
-
-       function __construct(TagNode $node, $sem) {
-               $this->node = $node;
-               $this->sem = $sem;
-       }
-
-       public function getRemovedDescription(ChangeText $txt) {
-               $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' );
-               if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){
-                       $tagDescription = "&lt;" . $this->node->qName . "&gt;";
-               }
-               if ($this->sem == TagToStringFactory::MOVED) {
-                       $txt->addHtml( wfMsgExt( 'diff-movedoutof', 'parseinline', $tagDescription ) );
-               } else if ($this->sem == TagToStringFactory::STYLE) {
-                       $txt->addHtml($tagDescription . ' ' . wfMsgExt( 'diff-styleremoved' , 'parseinline' ) );
-               } else {
-                       $txt->addHtml($tagDescription . ' ' . wfMsgExt( 'diff-removed' , 'parseinline' ) );
-               }
-               $this->addAttributes($txt, $this->node->attributes);
-               $txt->addHtml('.');
-       }
-
-       public function getAddedDescription(ChangeText $txt) {
-               $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' );
-               if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){
-                       $tagDescription = "&lt;" . $this->node->qName . "&gt;";
-               }
-               if ($this->sem == TagToStringFactory::MOVED) {
-                       $txt->addHtml( wfMsgExt( 'diff-movedto' , 'parseinline', $tagDescription) );
-               } else if ($this->sem == TagToStringFactory::STYLE) {
-                       $txt->addHtml($tagDescription . ' ' . wfMsgExt( 'diff-styleadded', 'parseinline' ) );
-               } else {
-                       $txt->addHtml($tagDescription . ' ' . wfMsgExt( 'diff-added', 'parseinline' ) );
-               }
-               $this->addAttributes($txt, $this->node->attributes);
-               $txt->addHtml('.');
-       }
-
-       protected function addAttributes(ChangeText $txt, array $attributes) {
-               if (count($attributes) < 1) {
-                       return;
-               }
-               $firstOne = true;
-               $nbAttributes_min_1 = count($attributes)-1;
-               $keys = array_keys($attributes);
-               for ($i=0;$i<$nbAttributes_min_1;$i++) {
-                       $key = $keys[$i];
-                       $attr = $attributes[$key];
-                       if($firstOne) {
-                               $firstOne = false;
-                               $txt->addHtml( wfMsgExt('diff-with', 'escapenoentities', $this->translateArgument($key), htmlspecialchars($attr) ) );
-                               continue;
-                       }
-                       $txt->addHtml( wfMsgExt( 'comma-separator', 'escapenoentities' ) .
-                               wfMsgExt( 'diff-with-additional', 'escapenoentities',
-                               $this->translateArgument( $key ), htmlspecialchars( $attr ) )
-                       );
-               }
-
-               if ($nbAttributes_min_1 > 0) {
-                       $txt->addHtml( wfMsgExt( 'diff-with-final', 'escapenoentities',
-                       $this->translateArgument($keys[$nbAttributes_min_1]),
-                       htmlspecialchars($attributes[$keys[$nbAttributes_min_1]]) ) );
-               }
-       }
-
-       protected function translateArgument($name) {
-               $translation = wfMsgExt('diff-' . $name, 'parseinline' );
-               if ( wfEmptyMsg( 'diff-' . $name, $translation ) ) {
-                       $translation = "&lt;" . $name . "&gt;";;
-               }
-               return htmlspecialchars( $translation );
-       }
-}
-
-class NoContentTagToString extends TagToString {
-
-       function __construct(TagNode $node, $sem) {
-               parent::__construct($node, $sem);
-       }
-
-       public function getAddedDescription(ChangeText $txt) {
-               $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' );
-               if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){
-                       $tagDescription = "&lt;" . $this->node->qName . "&gt;";
-               }
-               $txt->addHtml( wfMsgExt('diff-changedto', 'parseinline' ) . ' ' . $tagDescription);
-               $this->addAttributes($txt, $this->node->attributes);
-               $txt->addHtml('.');
-       }
-
-       public function getRemovedDescription(ChangeText $txt) {
-               $txt->addHtml( wfMsgExt('diff-changedfrom', 'parseinline' ) . ' ' . $tagDescription);
-               $this->addAttributes($txt, $this->node->attributes);
-               $txt->addHtml('.');
-       }
-}
-
-class AnchorToString extends TagToString {
-
-       function __construct(TagNode $node, $sem) {
-               parent::__construct($node, $sem);
-       }
-
-       protected function addAttributes(ChangeText $txt, array $attributes) {
-               if (array_key_exists('href', $attributes)) {
-                       $txt->addHtml(' ' . wfMsgExt( 'diff-withdestination', 'parseinline' ) . ' ' . htmlspecialchars($attributes['href']));
-                       unset($attributes['href']);
-               }
-               parent::addAttributes($txt, $attributes);
-       }
-}
-
-/**
- * Takes a branch root and creates an HTML file for it.
- */
-class HTMLOutput{
-
-       private $prefix;
-       private $handler;
-
-       function __construct($prefix, $handler) {
-               $this->prefix = $prefix;
-               $this->handler = $handler;
-       }
-
-       public function parse(TagNode $node) {
-               $handler = &$this->handler;
-
-               if (strcasecmp($node->qName, 'img') != 0 && strcasecmp($node->qName, 'body') != 0) {
-                       $handler->startElement($node->qName, $node->attributes);
-               }
-
-               $newStarted = false;
-               $remStarted = false;
-               $changeStarted = false;
-               $changeTXT = '';
-
-               foreach ($node->children as &$child) {
-                       if ($child instanceof TagNode) {
-                               if ($newStarted) {
-                                       $handler->endElement('span');
-                                       $newStarted = false;
-                               } else if ($changeStarted) {
-                                       $handler->endElement('span');
-                                       $changeStarted = false;
-                               } else if ($remStarted) {
-                                       $handler->endElement('span');
-                                       $remStarted = false;
-                               }
-                               $this->parse($child);
-                       } else if ($child instanceof TextNode) {
-                               $mod = $child->modification;
-
-                               if ($newStarted && ($mod->type != Modification::ADDED || $mod->firstOfID)) {
-                                       $handler->endElement('span');
-                                       $newStarted = false;
-                               } else if ($changeStarted && ($mod->type != Modification::CHANGED
-                                               || $mod->changes != $changeTXT || $mod->firstOfID)) {
-                                       $handler->endElement('span');
-                                       $changeStarted = false;
-                               } else if ($remStarted && ($mod->type != Modification::REMOVED || $mod ->firstOfID)) {
-                                       $handler->endElement('span');
-                                       $remStarted = false;
-                               }
-
-                               // no else because a removed part can just be closed and a new
-                               // part can start
-                               if (!$newStarted && $mod->type == Modification::ADDED) {
-                                       $attrs = array('class' => 'diff-html-added');
-                                       if ($mod->firstOfID) {
-                                               $attrs['id'] = "added-{$this->prefix}-{$mod->id}";
-                                       }
-                                       $handler->startElement('span', $attrs);
-                                       $newStarted = true;
-                               } else if (!$changeStarted && $mod->type == Modification::CHANGED) {
-                                       $attrs = array('class' => 'diff-html-changed');
-                                       if ($mod->firstOfID) {
-                                               $attrs['id'] = "changed-{$this->prefix}-{$mod->id}";
-                                       }
-                                       $handler->startElement('span', $attrs);
-
-                                       //tooltip
-                                       $handler->startElement('span', array('class' => 'tip'));
-                                       $handler->html($mod->changes);
-                                       $handler->endElement('span');
-
-                                       $changeStarted = true;
-                                       $changeTXT = $mod->changes;
-                               } else if (!$remStarted && $mod->type == Modification::REMOVED) {
-                                       $attrs = array('class'=>'diff-html-removed');
-                                       if ($mod->firstOfID) {
-                                               $attrs['id'] = "removed-{$this->prefix}-{$mod->id}";
-                                       }
-                                       $handler->startElement('span', $attrs);
-                                       $remStarted = true;
-                               }
-
-                               $chars = $child->text;
-
-                               if ($child instanceof ImageNode) {
-                                       $this->writeImage($child);
-                               } else {
-                                       $handler->characters($chars);
-                               }
-                       }
-               }
-
-               if ($newStarted) {
-                       $handler->endElement('span');
-                       $newStarted = false;
-               } else if ($changeStarted) {
-                       $handler->endElement('span');
-                       $changeStarted = false;
-               } else if ($remStarted) {
-                       $handler->endElement('span');
-                       $remStarted = false;
-               }
-
-               if (strcasecmp($node->qName, 'img') != 0
-                               && strcasecmp($node->qName, 'body') != 0) {
-                       $handler->endElement($node->qName);
-               }
-       }
-
-       private function writeImage(ImageNode  $imgNode) {
-               $attrs = $imgNode->attributes;
-               $this->handler->startElement('img', $attrs);
-               $this->handler->endElement('img');
-       }
-}
-
-class EchoingContentHandler {
-
-       function startElement($qname, /*array*/ $arguments) {
-               echo Xml::openElement($qname, $arguments);
-       }
-
-       function endElement($qname){
-               echo Xml::closeElement($qname);
-       }
-
-       function characters($chars){
-               echo htmlspecialchars($chars);
-       }
-
-       function html($html){
-               echo $html;
-       }
-
-}
-
-class DelegatingContentHandler {
-
-       private $delegate;
-
-       function __construct($delegate) {
-               $this->delegate = $delegate;
-       }
-
-       function startElement($qname, /*array*/ $arguments) {
-               $this->delegate->addHtml(Xml::openElement($qname, $arguments));
-       }
-
-       function endElement($qname){
-               $this->delegate->addHtml(Xml::closeElement($qname));
-       }
-
-       function characters($chars){
-               $this->delegate->addHtml(htmlspecialchars($chars));
-       }
-
-       function html($html){
-               $this->delegate->addHtml($html);
-       }
-}
diff --git a/includes/diff/Diff.php b/includes/diff/Diff.php
new file mode 100644 (file)
index 0000000..538c2d8
--- /dev/null
@@ -0,0 +1,580 @@
+<?php
+/* Copyright (C) 2008 Guy Van den Broeck <guy@guyvdb.eu>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * or see http://www.gnu.org/
+ */
+
+/**
+ * This diff implementation is mainly lifted from the LCS algorithm of the Eclipse project which
+ * in turn is based on Myers' "An O(ND) difference algorithm and its variations"
+ * (http://citeseer.ist.psu.edu/myers86ond.html) with range compression (see Wu et al.'s
+ * "An O(NP) Sequence Comparison Algorithm").
+ *
+ * This implementation supports an upper bound on the excution time.
+ *
+ * Complexity: O((M + N)D) worst case time, O(M + N + D^2) expected time, O(M + N) space
+ *
+ * @author Guy Van den Broeck
+ * @ingroup DifferenceEngine
+ */
+class WikiDiff3 {
+
+       //Input variables
+       private $from;
+       private $to;
+       private $m;
+       private $n;
+
+       private $tooLong;
+       private $powLimit;
+
+       //State variables
+       private $maxDifferences;
+       private $lcsLengthCorrectedForHeuristic = false;
+
+       //Output variables
+       public $length;
+       public $removed;
+       public $added;
+       public $heuristicUsed;
+
+       function __construct($tooLong = 2000000, $powLimit = 1.45){
+               $this->tooLong = $tooLong;
+               $this->powLimit = $powLimit;
+       }
+
+       public function diff(/*array*/ $from, /*array*/ $to){
+               //remember initial lengths
+               $m = sizeof($from);
+               $n = count($to);
+
+               $this->heuristicUsed = false;
+
+               //output
+               $removed = $m > 0 ? array_fill(0, $m, true) : array();
+               $added = $n > 0 ? array_fill(0, $n, true) : array();
+
+               //reduce the complexity for the next step (intentionally done twice)
+               //remove common tokens at the start
+               $i = 0;
+               while($i < $m && $i < $n && $from[$i] === $to[$i]) {
+                       $removed[$i] = $added[$i] = false;
+                       unset($from[$i], $to[$i]);
+                       ++$i;
+               }
+
+               //remove common tokens at the end
+               $j = 1;
+               while($i + $j <= $m && $i + $j <= $n && $from[$m - $j] === $to[$n - $j]) {
+                       $removed[$m - $j] = $added[$n - $j] = false;
+                       unset($from[$m - $j], $to[$n - $j]);
+                       ++$j;
+               }
+
+               $this->from = $newFromIndex = $this->to = $newToIndex = array();
+
+               //remove tokens not in both sequences
+               $shared = array();
+               foreach( $from as $key ) {
+                       $shared[$key] = false;
+               }
+
+               foreach($to as $index => &$el) {
+                       if(array_key_exists($el, $shared)) {
+                               //keep it
+                               $this->to[] = $el;
+                               $shared[$el] = true;
+                               $newToIndex[] = $index;
+                       }
+               }
+               foreach($from as $index => &$el) {
+                       if($shared[$el]) {
+                               //keep it
+                               $this->from[] = $el;
+                               $newFromIndex[] = $index;
+                       }
+               }
+
+               unset($shared, $from, $to);
+
+               $this->m = count($this->from);
+               $this->n = count($this->to);
+
+               $this->removed = $this->m > 0 ? array_fill(0, $this->m, true) : array();
+               $this->added = $this->n > 0 ? array_fill(0, $this->n, true) : array();
+
+               if ($this->m == 0 || $this->n == 0) {
+                       $this->length = 0;
+               } else {
+                       $this->maxDifferences = ceil(($this->m + $this->n) / 2.0);
+                       if ($this->m * $this->n > $this->tooLong) {
+                               // limit complexity to D^POW_LIMIT for long sequences
+                               $this->maxDifferences = floor(pow($this->maxDifferences, $this->powLimit - 1.0));
+                               wfDebug("Limiting max number of differences to $this->maxDifferences\n");
+                       }
+
+                       /*
+                        * The common prefixes and suffixes are always part of some LCS, include
+                        * them now to reduce our search space
+                        */
+                       $max = min($this->m, $this->n);
+                       for ($forwardBound = 0; $forwardBound < $max
+                                       && $this->from[$forwardBound] === $this->to[$forwardBound];
+                                       ++$forwardBound) {
+                               $this->removed[$forwardBound] = $this->added[$forwardBound] = false;
+                       }
+
+                       $backBoundL1 = $this->m - 1;
+                       $backBoundL2 = $this->n - 1;
+
+                       while ($backBoundL1 >= $forwardBound && $backBoundL2 >= $forwardBound
+                                       && $this->from[$backBoundL1] === $this->to[$backBoundL2]) {
+                               $this->removed[$backBoundL1--] = $this->added[$backBoundL2--] = false;
+                       }
+
+                       $temp = array_fill(0, $this->m + $this->n + 1, 0);
+                       $V = array($temp, $temp);
+                       $snake = array(0, 0, 0);
+
+                       $this->length = $forwardBound + $this->m - $backBoundL1 - 1
+                               + $this->lcs_rec($forwardBound, $backBoundL1,
+                               $forwardBound, $backBoundL2, $V, $snake);
+               }
+
+               $this->m = $m;
+               $this->n = $n;
+
+               $this->length += $i + $j - 1;
+
+               foreach($this->removed as $key => &$removed_elem) {
+                       if(!$removed_elem) {
+                               $removed[$newFromIndex[$key]] = false;
+                       }
+               }
+               foreach($this->added as $key => &$added_elem) {
+                       if(!$added_elem) {
+                               $added[$newToIndex[$key]] = false;
+                       }
+               }
+               $this->removed = $removed;
+               $this->added = $added;
+       }
+
+       function diff_range($from_lines, $to_lines) {
+               // Diff and store locally
+               $this->diff($from_lines, $to_lines);
+               unset($from_lines, $to_lines);
+
+               $ranges = array();
+               $xi = $yi = 0;
+               while ($xi < $this->m || $yi < $this->n) {
+                       // Matching "snake".
+                       while ($xi < $this->m && $yi < $this->n
+                                       && !$this->removed[$xi]
+                                       && !$this->added[$yi]) {
+                               ++$xi;
+                               ++$yi;
+                       }
+                       // Find deletes & adds.
+                       $xstart = $xi;
+                       while ($xi < $this->m && $this->removed[$xi]) {
+                               ++$xi;
+                       }
+
+                       $ystart = $yi;
+                       while ($yi < $this->n && $this->added[$yi]) {
+                               ++$yi;
+                       }
+
+                       if ($xi > $xstart || $yi > $ystart) {
+                               $ranges[] = new RangeDifference($xstart, $xi,
+                                                               $ystart, $yi);
+                       }
+               }
+               return $ranges;
+       }
+
+       private function lcs_rec($bottoml1, $topl1, $bottoml2, $topl2, &$V, &$snake) {
+               // check that both sequences are non-empty
+               if ($bottoml1 > $topl1 || $bottoml2 > $topl2) {
+                       return 0;
+               }
+
+               $d = $this->find_middle_snake($bottoml1, $topl1, $bottoml2,
+                                                       $topl2, $V, $snake);
+
+               // need to store these so we don't lose them when they're
+               // overwritten by the recursion
+               $len = $snake[2];
+               $startx = $snake[0];
+               $starty = $snake[1];
+
+               // the middle snake is part of the LCS, store it
+               for ($i = 0; $i < $len; ++$i) {
+                       $this->removed[$startx + $i] = $this->added[$starty + $i] = false;
+               }
+
+               if ($d > 1) {
+                       return $len
+                       + $this->lcs_rec($bottoml1, $startx - 1, $bottoml2,
+                                                       $starty - 1, $V, $snake)
+                       + $this->lcs_rec($startx + $len, $topl1, $starty + $len,
+                                                       $topl2, $V, $snake);
+               } else if ($d == 1) {
+                       /*
+                        * In this case the sequences differ by exactly 1 line. We have
+                        * already saved all the lines after the difference in the for loop
+                        * above, now we need to save all the lines before the difference.
+                        */
+                       $max = min($startx - $bottoml1, $starty - $bottoml2);
+                       for ($i = 0; $i < $max; ++$i) {
+                               $this->removed[$bottoml1 + $i] =
+                                       $this->added[$bottoml2 + $i] = false;
+                       }
+                       return $max + $len;
+               }
+               return $len;
+       }
+
+       private function find_middle_snake($bottoml1, $topl1, $bottoml2,$topl2, &$V, &$snake) {
+               $from = &$this->from;
+               $to = &$this->to;
+               $V0 = &$V[0];
+               $V1 = &$V[1];
+               $snake0 = &$snake[0];
+               $snake1 = &$snake[1];
+               $snake2 = &$snake[2];
+               $bottoml1_min_1 = $bottoml1-1;
+               $bottoml2_min_1 = $bottoml2-1;
+               $N = $topl1 - $bottoml1_min_1;
+               $M = $topl2 - $bottoml2_min_1;
+               $delta = $N - $M;
+               $maxabsx = $N+$bottoml1;
+               $maxabsy = $M+$bottoml2;
+               $limit = min($this->maxDifferences, ceil(($N + $M ) / 2));
+
+               //value_to_add_forward: a 0 or 1 that we add to the start
+               // offset to make it odd/even
+               if (($M & 1) == 1) {
+                       $value_to_add_forward = 1;
+               } else {
+                       $value_to_add_forward = 0;
+               }
+
+               if (($N & 1) == 1) {
+                       $value_to_add_backward = 1;
+               } else {
+                       $value_to_add_backward = 0;
+               }
+
+               $start_forward = -$M;
+               $end_forward = $N;
+               $start_backward = -$N;
+               $end_backward = $M;
+
+               $limit_min_1 = $limit - 1;
+               $limit_plus_1 = $limit + 1;
+
+               $V0[$limit_plus_1] = 0;
+               $V1[$limit_min_1] = $N;
+               $limit = min($this->maxDifferences, ceil(($N + $M ) / 2));
+
+               if (($delta & 1) == 1) {
+                       for ($d = 0; $d <= $limit; ++$d) {
+                               $start_diag = max($value_to_add_forward + $start_forward, -$d);
+                               $end_diag = min($end_forward, $d);
+                               $value_to_add_forward = 1 - $value_to_add_forward;
+
+                               // compute forward furthest reaching paths
+                               for ($k = $start_diag; $k <= $end_diag; $k += 2) {
+                                       if ($k == -$d || ($k < $d
+                                                       && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k])) {
+                                               $x = $V0[$limit_plus_1 + $k];
+                                       } else {
+                                               $x = $V0[$limit_min_1 + $k] + 1;
+                                       }
+
+                                       $absx = $snake0 = $x + $bottoml1;
+                                       $absy = $snake1 = $x - $k + $bottoml2;
+
+                                       while ($absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy]) {
+                                               ++$absx;
+                                               ++$absy;
+                                       }
+                                       $x = $absx-$bottoml1;
+
+                                       $snake2 = $absx -$snake0;
+                                       $V0[$limit + $k] = $x;
+                                       if ($k >= $delta - $d + 1 && $k <= $delta + $d - 1
+                                                       && $x >= $V1[$limit + $k - $delta]) {
+                                               return 2 * $d - 1;
+                                       }
+
+                                       // check to see if we can cut down the diagonal range
+                                       if ($x >= $N && $end_forward > $k - 1) {
+                                               $end_forward = $k - 1;
+                                       } else if ($absy - $bottoml2 >= $M) {
+                                               $start_forward = $k + 1;
+                                               $value_to_add_forward = 0;
+                                       }
+                               }
+
+                               $start_diag = max($value_to_add_backward + $start_backward, -$d);
+                               $end_diag = min($end_backward, $d);
+                               $value_to_add_backward = 1 - $value_to_add_backward;
+
+                               // compute backward furthest reaching paths
+                               for ($k = $start_diag; $k <= $end_diag; $k += 2) {
+                                       if ($k == $d
+                                       || ($k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k])) {
+                                               $x = $V1[$limit_min_1 + $k];
+                                       } else {
+                                               $x = $V1[$limit_plus_1 + $k] - 1;
+                                       }
+
+                                       $y = $x - $k - $delta;
+
+                                       $snake2 = 0;
+                                       while ($x > 0 && $y > 0
+                                       && $from[$x +$bottoml1_min_1] === $to[$y + $bottoml2_min_1]) {
+                                               --$x;
+                                               --$y;
+                                               ++$snake2;
+                                       }
+                                       $V1[$limit + $k] = $x;
+
+                                       // check to see if we can cut down our diagonal range
+                                       if ($x <= 0) {
+                                               $start_backward = $k + 1;
+                                               $value_to_add_backward = 0;
+                                       } else if ($y <= 0 && $end_backward > $k - 1) {
+                                               $end_backward = $k - 1;
+                                       }
+                               }
+                       }
+               } else {
+                       for ($d = 0; $d <= $limit; ++$d) {
+                               $start_diag = max($value_to_add_forward + $start_forward, -$d);
+                               $end_diag = min($end_forward, $d);
+                               $value_to_add_forward = 1 - $value_to_add_forward;
+
+                               // compute forward furthest reaching paths
+                               for ($k = $start_diag; $k <= $end_diag; $k += 2) {
+                                       if ($k == -$d
+                                       || ($k < $d && $V0[$limit_min_1 + $k] < $V0[$limit_plus_1 + $k])) {
+                                               $x = $V0[$limit_plus_1 + $k];
+                                       } else {
+                                               $x = $V0[$limit_min_1 + $k] + 1;
+                                       }
+
+                                       $absx = $snake0 = $x + $bottoml1;
+                                       $absy = $snake1 = $x - $k + $bottoml2;
+
+                                       while ($absx < $maxabsx && $absy < $maxabsy && $from[$absx] === $to[$absy]) {
+                                               ++$absx;
+                                               ++$absy;
+                                       }
+                                       $x = $absx-$bottoml1;
+                                       $snake2 = $absx -$snake0;
+                                       $V0[$limit + $k] = $x;
+
+                                       // check to see if we can cut down the diagonal range
+                                       if ($x >= $N && $end_forward > $k - 1) {
+                                               $end_forward = $k - 1;
+                                       } else if ($absy-$bottoml2 >= $M) {
+                                               $start_forward = $k + 1;
+                                               $value_to_add_forward = 0;
+                                       }
+                               }
+
+                               $start_diag = max($value_to_add_backward + $start_backward, -$d);
+                               $end_diag = min($end_backward, $d);
+                               $value_to_add_backward = 1 - $value_to_add_backward;
+
+                               // compute backward furthest reaching paths
+                               for ($k = $start_diag; $k <= $end_diag; $k += 2) {
+                                       if ($k == $d
+                                       || ($k != -$d && $V1[$limit_min_1 + $k] < $V1[$limit_plus_1 + $k])) {
+                                               $x = $V1[$limit_min_1 + $k];
+                                       } else {
+                                               $x = $V1[$limit_plus_1 + $k] - 1;
+                                       }
+
+                                       $y = $x - $k - $delta;
+
+                                       $snake2 = 0;
+                                       while ($x > 0 && $y > 0
+                                                       && $from[$x +$bottoml1_min_1] === $to[$y + $bottoml2_min_1]) {
+                                               --$x;
+                                               --$y;
+                                               ++$snake2;
+                                       }
+                                       $V1[$limit + $k] = $x;
+
+                                       if ($k >= -$delta - $d && $k <= $d - $delta
+                                                       && $x <= $V0[$limit + $k + $delta]) {
+                                               $snake0 = $bottoml1 + $x;
+                                               $snake1 = $bottoml2 + $y;
+                                               return 2 * $d;
+                                       }
+
+                                       // check to see if we can cut down our diagonal range
+                                       if ($x <= 0) {
+                                               $start_backward = $k + 1;
+                                               $value_to_add_backward = 0;
+                                       } else if ($y <= 0 && $end_backward > $k - 1) {
+                                               $end_backward = $k - 1;
+                                       }
+                               }
+                       }
+               }
+               /*
+                * computing the true LCS is too expensive, instead find the diagonal
+                * with the most progress and pretend a midle snake of length 0 occurs
+                * there.
+                */
+
+               $most_progress = self::findMostProgress($M, $N, $limit, $V);
+
+               $snake0 = $bottoml1 + $most_progress[0];
+               $snake1 = $bottoml2 + $most_progress[1];
+               $snake2 = 0;
+               wfDebug("Computing the LCS is too expensive. Using a heuristic.\n");
+               $this->heuristicUsed = true;
+               return 5; /*
+               * HACK: since we didn't really finish the LCS computation
+               * we don't really know the length of the SES. We don't do
+               * anything with the result anyway, unless it's <=1. We know
+               * for a fact SES > 1 so 5 is as good a number as any to
+               * return here
+               */
+       }
+
+       private static function findMostProgress($M, $N, $limit, $V) {
+               $delta = $N - $M;
+
+               if (($M & 1) == ($limit & 1)) {
+                       $forward_start_diag = max(-$M, -$limit);
+               } else {
+                       $forward_start_diag = max(1 - $M, -$limit);
+               }
+
+               $forward_end_diag = min($N, $limit);
+
+               if (($N & 1) == ($limit & 1)) {
+                       $backward_start_diag = max(-$N, -$limit);
+               } else {
+                       $backward_start_diag = max(1 - $N, -$limit);
+               }
+
+               $backward_end_diag = -min($M, $limit);
+
+               $temp = array(0, 0, 0);
+
+
+               $max_progress = array_fill(0, ceil(max($forward_end_diag - $forward_start_diag,
+                               $backward_end_diag - $backward_start_diag) / 2), $temp);
+               $num_progress = 0; // the 1st entry is current, it is initialized
+               // with 0s
+
+               // first search the forward diagonals
+               for ($k = $forward_start_diag; $k <= $forward_end_diag; $k += 2) {
+                       $x = $V[0][$limit + $k];
+                       $y = $x - $k;
+                       if ($x > $N || $y > $M) {
+                               continue;
+                       }
+
+                       $progress = $x + $y;
+                       if ($progress > $max_progress[0][2]) {
+                               $num_progress = 0;
+                               $max_progress[0][0] = $x;
+                               $max_progress[0][1] = $y;
+                               $max_progress[0][2] = $progress;
+                       } else if ($progress == $max_progress[0][2]) {
+                               ++$num_progress;
+                               $max_progress[$num_progress][0] = $x;
+                               $max_progress[$num_progress][1] = $y;
+                               $max_progress[$num_progress][2] = $progress;
+                       }
+               }
+
+               $max_progress_forward = true; // initially the maximum
+               // progress is in the forward
+               // direction
+
+               // now search the backward diagonals
+               for ($k = $backward_start_diag; $k <= $backward_end_diag; $k += 2) {
+                       $x = $V[1][$limit + $k];
+                       $y = $x - $k - $delta;
+                       if ($x < 0 || $y < 0) {
+                               continue;
+                       }
+
+                       $progress = $N - $x + $M - $y;
+                       if ($progress > $max_progress[0][2]) {
+                               $num_progress = 0;
+                               $max_progress_forward = false;
+                               $max_progress[0][0] = $x;
+                               $max_progress[0][1] = $y;
+                               $max_progress[0][2] = $progress;
+                       } else if ($progress == $max_progress[0][2] && !$max_progress_forward) {
+                               ++$num_progress;
+                               $max_progress[$num_progress][0] = $x;
+                               $max_progress[$num_progress][1] = $y;
+                               $max_progress[$num_progress][2] = $progress;
+                       }
+               }
+
+               // return the middle diagonal with maximal progress.
+               return $max_progress[floor($num_progress / 2)];
+       }
+
+       public function getLcsLength(){
+               if($this->heuristicUsed && !$this->lcsLengthCorrectedForHeuristic){
+                       $this->lcsLengthCorrectedForHeuristic = true;
+                       $this->length = $this->m-array_sum($this->added);
+               }
+               return $this->length;
+       }
+
+}
+
+/**
+ * Alternative representation of a set of changes, by the index
+ * ranges that are changed.
+ * 
+ * @ingroup DifferenceEngine
+ */
+class RangeDifference {
+
+       public $leftstart;
+       public $leftend;
+       public $leftlength;
+
+       public $rightstart;
+       public $rightend;
+       public $rightlength;
+
+       function __construct($leftstart, $leftend, $rightstart, $rightend){
+               $this->leftstart = $leftstart;
+               $this->leftend = $leftend;
+               $this->leftlength = $leftend - $leftstart;
+               $this->rightstart = $rightstart;
+               $this->rightend = $rightend;
+               $this->rightlength = $rightend - $rightstart;
+       }
+}
diff --git a/includes/diff/DifferenceEngine.php b/includes/diff/DifferenceEngine.php
new file mode 100644 (file)
index 0000000..0f03058
--- /dev/null
@@ -0,0 +1,2115 @@
+<?php
+/**
+ * @defgroup DifferenceEngine DifferenceEngine
+ */
+
+/**
+ * Constant to indicate diff cache compatibility.
+ * Bump this when changing the diff formatting in a way that
+ * fixes important bugs or such to force cached diff views to
+ * clear.
+ */
+define( 'MW_DIFF_VERSION', '1.11a' );
+
+/**
+ * @todo document
+ * @ingroup DifferenceEngine
+ */
+class DifferenceEngine {
+       /**#@+
+        * @private
+        */
+       var $mOldid, $mNewid, $mTitle;
+       var $mOldtitle, $mNewtitle, $mPagetitle;
+       var $mOldtext, $mNewtext;
+       var $mOldPage, $mNewPage;
+       var $mRcidMarkPatrolled;
+       var $mOldRev, $mNewRev;
+       var $mRevisionsLoaded = false; // Have the revisions been loaded
+       var $mTextLoaded = 0; // How many text blobs have been loaded, 0, 1 or 2?
+       var $htmldiff;
+       /**#@-*/
+
+       /**
+        * Constructor
+        * @param $titleObj Title object that the diff is associated with
+        * @param $old Integer: old ID we want to show and diff with.
+        * @param $new String: either 'prev' or 'next'.
+        * @param $rcid Integer: ??? FIXME (default 0)
+        * @param $refreshCache boolean If set, refreshes the diff cache
+        * @param $htmldiff boolean If set, output using HTMLDiff instead of raw wikicode diff
+        */
+       function __construct( $titleObj = null, $old = 0, $new = 0, $rcid = 0, $refreshCache = false , $htmldiff = false) {
+               $this->mTitle = $titleObj;
+               wfDebug("DifferenceEngine old '$old' new '$new' rcid '$rcid'\n");
+
+               if ( 'prev' === $new ) {
+                       # Show diff between revision $old and the previous one.
+                       # Get previous one from DB.
+                       #
+                       $this->mNewid = intval($old);
+
+                       $this->mOldid = $this->mTitle->getPreviousRevisionID( $this->mNewid );
+
+               } elseif ( 'next' === $new ) {
+                       # Show diff between revision $old and the previous one.
+                       # Get previous one from DB.
+                       #
+                       $this->mOldid = intval($old);
+                       $this->mNewid = $this->mTitle->getNextRevisionID( $this->mOldid );
+                       if ( false === $this->mNewid ) {
+                               # if no result, NewId points to the newest old revision. The only newer
+                               # revision is cur, which is "0".
+                               $this->mNewid = 0;
+                       }
+
+               } else {
+                       $this->mOldid = intval($old);
+                       $this->mNewid = intval($new);
+               }
+               $this->mRcidMarkPatrolled = intval($rcid);  # force it to be an integer
+               $this->mRefreshCache = $refreshCache;
+               $this->htmldiff = $htmldiff;
+       }
+
+       function getTitle() {
+               return $this->mTitle;
+       }
+
+       function showDiffPage( $diffOnly = false ) {
+               global $wgUser, $wgOut, $wgUseExternalEditor, $wgUseRCPatrol, $wgEnableHtmlDiff;
+               wfProfileIn( __METHOD__ );
+
+
+               # If external diffs are enabled both globally and for the user,
+               # we'll use the application/x-external-editor interface to call
+               # an external diff tool like kompare, kdiff3, etc.
+               if($wgUseExternalEditor && $wgUser->getOption('externaldiff')) {
+                       global $wgInputEncoding,$wgServer,$wgScript,$wgLang;
+                       $wgOut->disable();
+                       header ( "Content-type: application/x-external-editor; charset=".$wgInputEncoding );
+                       $url1=$this->mTitle->getFullURL("action=raw&oldid=".$this->mOldid);
+                       $url2=$this->mTitle->getFullURL("action=raw&oldid=".$this->mNewid);
+                       $special=$wgLang->getNsText(NS_SPECIAL);
+                       $control=<<<CONTROL
+                       [Process]
+                       Type=Diff text
+                       Engine=MediaWiki
+                       Script={$wgServer}{$wgScript}
+                       Special namespace={$special}
+
+                       [File]
+                       Extension=wiki
+                       URL=$url1
+
+                       [File 2]
+                       Extension=wiki
+                       URL=$url2
+CONTROL;
+                       echo($control);
+                       return;
+               }
+
+               $wgOut->setArticleFlag( false );
+               if ( ! $this->loadRevisionData() ) {
+                       $t = $this->mTitle->getPrefixedText();
+                       $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
+                       $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
+                       $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
+                       wfProfileOut( __METHOD__ );
+                       return;
+               }
+
+               wfRunHooks( 'DiffViewHeader', array( $this, $this->mOldRev, $this->mNewRev ) );
+
+               if ( $this->mNewRev->isCurrent() ) {
+                       $wgOut->setArticleFlag( true );
+               }
+
+               # mOldid is false if the difference engine is called with a "vague" query for
+               # a diff between a version V and its previous version V' AND the version V
+               # is the first version of that article. In that case, V' does not exist.
+               if ( $this->mOldid === false ) {
+                       $this->showFirstRevision();
+                       $this->renderNewRevision();  // should we respect $diffOnly here or not?
+                       wfProfileOut( __METHOD__ );
+                       return;
+               }
+
+               $wgOut->suppressQuickbar();
+
+               $oldTitle = $this->mOldPage->getPrefixedText();
+               $newTitle = $this->mNewPage->getPrefixedText();
+               if( $oldTitle == $newTitle ) {
+                       $wgOut->setPageTitle( $newTitle );
+               } else {
+                       $wgOut->setPageTitle( $oldTitle . ', ' . $newTitle );
+               }
+               $wgOut->setSubtitle( wfMsg( 'difference' ) );
+               $wgOut->setRobotPolicy( 'noindex,nofollow' );
+
+               if ( !( $this->mOldPage->userCanRead() && $this->mNewPage->userCanRead() ) ) {
+                       $wgOut->loginToUse();
+                       $wgOut->output();
+                       wfProfileOut( __METHOD__ );
+                       exit;
+               }
+
+               $sk = $wgUser->getSkin();
+
+               // Check if page is editable
+               $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
+               if ( $editable && $this->mNewRev->isCurrent() && $wgUser->isAllowed( 'rollback' ) ) {
+                       $rollback = '&nbsp;&nbsp;&nbsp;' . $sk->generateRollback( $this->mNewRev );
+               } else {
+                       $rollback = '';
+               }
+
+               // Prepare a change patrol link, if applicable
+               if( $wgUseRCPatrol && $wgUser->isAllowed( 'patrol' ) ) {
+                       // If we've been given an explicit change identifier, use it; saves time
+                       if( $this->mRcidMarkPatrolled ) {
+                               $rcid = $this->mRcidMarkPatrolled;
+                       } else {
+                               // Look for an unpatrolled change corresponding to this diff
+                               $db = wfGetDB( DB_SLAVE );
+                               $change = RecentChange::newFromConds(
+                               array(
+                               // Add redundant user,timestamp condition so we can use the existing index
+                                               'rc_user_text'  => $this->mNewRev->getRawUserText(),
+                                               'rc_timestamp' => $db->timestamp( $this->mNewRev->getTimestamp() ),
+                                               'rc_this_oldid' => $this->mNewid,
+                                               'rc_last_oldid' => $this->mOldid,
+                                               'rc_patrolled' => 0
+                               ),
+                               __METHOD__
+                               );
+                               if( $change instanceof RecentChange ) {
+                                       $rcid = $change->mAttribs['rc_id'];
+                               } else {
+                                       // None found
+                                       $rcid = 0;
+                               }
+                       }
+                       // Build the link
+                       if( $rcid ) {
+                               $patrol = ' <span class="patrollink">[' . $sk->makeKnownLinkObj(
+                               $this->mTitle,
+                               wfMsgHtml( 'markaspatrolleddiff' ),
+                                       "action=markpatrolled&rcid={$rcid}"
+                               ) . ']</span>';
+                       } else {
+                               $patrol = '';
+                       }
+               } else {
+                       $patrol = '';
+               }
+
+               $htmldiffarg = $this->htmlDiffArgument();
+               $prevlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'previousdiff' ),
+                       'diff=prev&oldid='.$this->mOldid.$htmldiffarg, '', '', 'id="differences-prevlink"' );
+               if ( $this->mNewRev->isCurrent() ) {
+                       $nextlink = '&nbsp;';
+               } else {
+                       $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ),
+                               'diff=next&oldid='.$this->mNewid.$htmldiffarg, '', '', 'id="differences-nextlink"' );
+               }
+
+               $oldminor = '';
+               $newminor = '';
+
+               if ($this->mOldRev->mMinorEdit == 1) {
+                       $oldminor = Xml::span( wfMsg( 'minoreditletter'), 'minor' ) . ' ';
+               }
+
+               if ($this->mNewRev->mMinorEdit == 1) {
+                       $newminor = Xml::span( wfMsg( 'minoreditletter'), 'minor' ) . ' ';
+               }
+
+               $rdel = ''; $ldel = '';
+               if( $wgUser->isAllowed( 'deleterevision' ) ) {
+                       $revdel = SpecialPage::getTitleFor( 'Revisiondelete' );
+                       if( !$this->mOldRev->userCan( Revision::DELETED_RESTRICTED ) ) {
+                               // If revision was hidden from sysops
+                               $ldel = wfMsgHtml('rev-delundel');
+                       } else {
+                               $ldel = $sk->makeKnownLinkObj( $revdel,
+                               wfMsgHtml('rev-delundel'),
+                                       'target=' . urlencode( $this->mOldRev->mTitle->getPrefixedDbkey() ) .
+                                       '&oldid=' . urlencode( $this->mOldRev->getId() ) );
+                               // Bolden oversighted content
+                               if( $this->mOldRev->isDeleted( Revision::DELETED_RESTRICTED ) )
+                               $ldel = "<strong>$ldel</strong>";
+                       }
+                       $ldel = "&nbsp;&nbsp;&nbsp;<tt>(<small>$ldel</small>)</tt> ";
+                       // We don't currently handle well changing the top revision's settings
+                       if( $this->mNewRev->isCurrent() ) {
+                               // If revision was hidden from sysops
+                               $rdel = wfMsgHtml('rev-delundel');
+                       } else if( !$this->mNewRev->userCan( Revision::DELETED_RESTRICTED ) ) {
+                               // If revision was hidden from sysops
+                               $rdel = wfMsgHtml('rev-delundel');
+                       } else {
+                               $rdel = $sk->makeKnownLinkObj( $revdel,
+                               wfMsgHtml('rev-delundel'),
+                                       'target=' . urlencode( $this->mNewRev->mTitle->getPrefixedDbkey() ) .
+                                       '&oldid=' . urlencode( $this->mNewRev->getId() ) );
+                               // Bolden oversighted content
+                               if( $this->mNewRev->isDeleted( Revision::DELETED_RESTRICTED ) )
+                               $rdel = "<strong>$rdel</strong>";
+                       }
+                       $rdel = "&nbsp;&nbsp;&nbsp;<tt>(<small>$rdel</small>)</tt> ";
+               }
+
+               $oldHeader = '<div id="mw-diff-otitle1"><strong>'.$this->mOldtitle.'</strong></div>' .
+                       '<div id="mw-diff-otitle2">' . $sk->revUserTools( $this->mOldRev, true ) . "</div>" .
+                       '<div id="mw-diff-otitle3">' . $oldminor . $sk->revComment( $this->mOldRev, !$diffOnly, true ) . $ldel . "</div>" .
+                       '<div id="mw-diff-otitle4">' . $prevlink .'</div>';
+               $newHeader = '<div id="mw-diff-ntitle1"><strong>'.$this->mNewtitle.'</strong></div>' .
+                       '<div id="mw-diff-ntitle2">' . $sk->revUserTools( $this->mNewRev, true ) . " $rollback</div>" .
+                       '<div id="mw-diff-ntitle3">' . $newminor . $sk->revComment( $this->mNewRev, !$diffOnly, true ) . $rdel . "</div>" .
+                       '<div id="mw-diff-ntitle4">' . $nextlink . $patrol . '</div>';
+
+               if( $wgEnableHtmlDiff && $this->htmldiff) {
+                       $multi = $this->getMultiNotice();
+                       $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'wikicodecomparison' ),
+                       'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=0', '', '', 'id="differences-switchtype"' ).'</div>');
+                       $wgOut->addHTML( $this->addHeader( '', $oldHeader, $newHeader, $multi ) );
+                       $this->renderHtmlDiff();
+               } else {
+                       if($wgEnableHtmlDiff){
+                               $wgOut->addHTML('<div class="diff-switchtype">'.$sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'visualcomparison' ),
+                               'diff='.$this->mNewid.'&oldid='.$this->mOldid.'&htmldiff=1', '', '', 'id="differences-switchtype"' ).'</div>');
+                       }
+                       $this->showDiff( $oldHeader, $newHeader );
+                       if( !$diffOnly ) {
+                               $this->renderNewRevision();
+                       }
+               }
+               wfProfileOut( __METHOD__ );
+       }
+
+       /**
+        * Show the new revision of the page.
+        */
+       function renderNewRevision() {
+               global $wgOut;
+               wfProfileIn( __METHOD__ );
+
+               $wgOut->addHTML( "<hr /><h2>{$this->mPagetitle}</h2>\n" );
+               #add deleted rev tag if needed
+               if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
+                       $wgOut->addWikiMsg( 'rev-deleted-text-permission' );
+               } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
+                       $wgOut->addWikiMsg( 'rev-deleted-text-view' );
+               }
+
+               if( !$this->mNewRev->isCurrent() ) {
+                       $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
+               }
+
+               $this->loadNewText();
+               if( is_object( $this->mNewRev ) ) {
+                       $wgOut->setRevisionId( $this->mNewRev->getId() );
+               }
+
+               if ($this->mTitle->isCssJsSubpage() || $this->mTitle->isCssOrJsPage()) {
+                       // Stolen from Article::view --AG 2007-10-11
+
+                       // Give hooks a chance to customise the output
+                       if( wfRunHooks( 'ShowRawCssJs', array( $this->mNewtext, $this->mTitle, $wgOut ) ) ) {
+                               // Wrap the whole lot in a <pre> and don't parse
+                               $m = array();
+                               preg_match( '!\.(css|js)$!u', $this->mTitle->getText(), $m );
+                               $wgOut->addHtml( "<pre class=\"mw-code mw-{$m[1]}\" dir=\"ltr\">\n" );
+                               $wgOut->addHtml( htmlspecialchars( $this->mNewtext ) );
+                               $wgOut->addHtml( "\n</pre>\n" );
+                       }
+               } else
+               $wgOut->addWikiTextTidy( $this->mNewtext );
+
+               if( !$this->mNewRev->isCurrent() ) {
+                       $wgOut->parserOptions()->setEditSection( $oldEditSectionSetting );
+               }
+
+               wfProfileOut( __METHOD__ );
+       }
+
+
+       function renderHtmlDiff() {
+               global $wgOut, $wgTitle, $wgParser, $wgDebugComments;
+               wfProfileIn( __METHOD__ );
+
+               $this->showDiffStyle();
+
+               $wgOut->addHTML( '<h2>'.wfMsgHtml( 'visual-comparison' )."</h2>\n" );
+               #add deleted rev tag if needed
+               if( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
+                       $wgOut->addWikiMsg( 'rev-deleted-text-permission' );
+               } else if( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
+                       $wgOut->addWikiMsg( 'rev-deleted-text-view' );
+               }
+
+               if( !$this->mNewRev->isCurrent() ) {
+                       $oldEditSectionSetting = $wgOut->parserOptions()->setEditSection( false );
+               }
+
+               $this->loadText();
+
+               // Old revision
+               if( is_object( $this->mOldRev ) ) {
+                       $wgOut->setRevisionId( $this->mOldRev->getId() );
+               }
+
+               $popts = $wgOut->parserOptions();
+               $oldTidy = $popts->setTidy( true );
+               $popts->setEditSection( false );
+
+               $parserOutput = $wgParser->parse( $this->mOldtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
+               $popts->setTidy( $oldTidy );
+
+               //only for new?
+               //$wgOut->addParserOutputNoText( $parserOutput );
+               $oldHtml = $parserOutput->getText();
+               wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$oldHtml ) );
+
+               // New revision
+               if( is_object( $this->mNewRev ) ) {
+                       $wgOut->setRevisionId( $this->mNewRev->getId() );
+               }
+
+               $popts = $wgOut->parserOptions();
+               $oldTidy = $popts->setTidy( true );
+
+               $parserOutput = $wgParser->parse( $this->mNewtext, $wgTitle, $popts, true, true, $wgOut->getRevisionId() );
+               $popts->setTidy( $oldTidy );
+
+               $wgOut->addParserOutputNoText( $parserOutput );
+               $newHtml = $parserOutput->getText();
+               wfRunHooks( 'OutputPageBeforeHTML', array( &$wgOut, &$newHtml ) );
+
+               unset($parserOutput, $popts);
+
+               $differ = new HTMLDiffer(new DelegatingContentHandler($wgOut));
+               $differ->htmlDiff($oldHtml, $newHtml);
+               if ( $wgDebugComments ) {
+                       $wgOut->addHtml( "\n<!-- HtmlDiff Debug Output:\n" . HTMLDiffer::getDebugOutput() . " End Debug -->" );
+               }
+
+               wfProfileOut( __METHOD__ );
+       }
+
+       /**
+        * Show the first revision of an article. Uses normal diff headers in
+        * contrast to normal "old revision" display style.
+        */
+       function showFirstRevision() {
+               global $wgOut, $wgUser;
+               wfProfileIn( __METHOD__ );
+
+               # Get article text from the DB
+               #
+               if ( ! $this->loadNewText() ) {
+                       $t = $this->mTitle->getPrefixedText();
+                       $d = wfMsgExt( 'missingarticle-diff', array( 'escape' ), $this->mOldid, $this->mNewid );
+                       $wgOut->setPagetitle( wfMsg( 'errorpagetitle' ) );
+                       $wgOut->addWikiMsg( 'missing-article', "<nowiki>$t</nowiki>", $d );
+                       wfProfileOut( __METHOD__ );
+                       return;
+               }
+               if ( $this->mNewRev->isCurrent() ) {
+                       $wgOut->setArticleFlag( true );
+               }
+
+               # Check if user is allowed to look at this page. If not, bail out.
+               #
+               if ( !( $this->mTitle->userCanRead() ) ) {
+                       $wgOut->loginToUse();
+                       $wgOut->output();
+                       wfProfileOut( __METHOD__ );
+                       exit;
+               }
+
+               # Prepare the header box
+               #
+               $sk = $wgUser->getSkin();
+
+               $nextlink = $sk->makeKnownLinkObj( $this->mTitle, wfMsgHtml( 'nextdiff' ), 'diff=next&oldid='.$this->mNewid.$this->htmlDiffArgument(), '', '', 'id="differences-nextlink"' );
+               $header = "<div class=\"firstrevisionheader\" style=\"text-align: center\"><strong>{$this->mOldtitle}</strong><br />" .
+               $sk->revUserTools( $this->mNewRev ) . "<br />" .
+               $sk->revComment( $this->mNewRev ) . "<br />" .
+               $nextlink . "</div>\n";
+
+               $wgOut->addHTML( $header );
+
+               $wgOut->setSubtitle( wfMsg( 'difference' ) );
+               $wgOut->setRobotPolicy( 'noindex,nofollow' );
+
+               wfProfileOut( __METHOD__ );
+       }
+
+       function htmlDiffArgument(){
+               global $wgEnableHtmlDiff;
+               if($wgEnableHtmlDiff){
+                       if($this->htmldiff){
+                               return '&htmldiff=1';
+                       }else{
+                               return '&htmldiff=0';
+                       }
+               }else{
+                       return '';
+               }
+       }
+
+       /**
+        * Get the diff text, send it to $wgOut
+        * Returns false if the diff could not be generated, otherwise returns true
+        */
+       function showDiff( $otitle, $ntitle ) {
+               global $wgOut;
+               $diff = $this->getDiff( $otitle, $ntitle );
+               if ( $diff === false ) {
+                       $wgOut->addWikiMsg( 'missing-article', "<nowiki>(fixme, bug)</nowiki>", '' );
+                       return false;
+               } else {
+                       $this->showDiffStyle();
+                       $wgOut->addHTML( $diff );
+                       return true;
+               }
+       }
+
+       /**
+        * Add style sheets and supporting JS for diff display.
+        */
+       function showDiffStyle() {
+               global $wgStylePath, $wgStyleVersion, $wgOut;
+               $wgOut->addStyle( 'common/diff.css' );
+
+               // JS is needed to detect old versions of Mozilla to work around an annoyance bug.
+               $wgOut->addScript( "<script type=\"text/javascript\" src=\"$wgStylePath/common/diff.js?$wgStyleVersion\"></script>" );
+       }
+
+       /**
+        * Get complete diff table, including header
+        *
+        * @param Title $otitle Old title
+        * @param Title $ntitle New title
+        * @return mixed
+        */
+       function getDiff( $otitle, $ntitle ) {
+               $body = $this->getDiffBody();
+               if ( $body === false ) {
+                       return false;
+               } else {
+                       $multi = $this->getMultiNotice();
+                       return $this->addHeader( $body, $otitle, $ntitle, $multi );
+               }
+       }
+
+       /**
+        * Get the diff table body, without header
+        *
+        * @return mixed
+        */
+       function getDiffBody() {
+               global $wgMemc;
+               wfProfileIn( __METHOD__ );
+               // Check if the diff should be hidden from this user
+               if ( $this->mOldRev && !$this->mOldRev->userCan(Revision::DELETED_TEXT) ) {
+                       return '';
+               } else if ( $this->mNewRev && !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
+                       return '';
+               }
+               // Cacheable?
+               $key = false;
+               if ( $this->mOldid && $this->mNewid ) {
+                       $key = wfMemcKey( 'diff', 'version', MW_DIFF_VERSION, 'oldid', $this->mOldid, 'newid', $this->mNewid );
+                       // Try cache
+                       if ( !$this->mRefreshCache ) {
+                               $difftext = $wgMemc->get( $key );
+                               if ( $difftext ) {
+                                       wfIncrStats( 'diff_cache_hit' );
+                                       $difftext = $this->localiseLineNumbers( $difftext );
+                                       $difftext .= "\n<!-- diff cache key $key -->\n";
+                                       wfProfileOut( __METHOD__ );
+                                       return $difftext;
+                               }
+                       } // don't try to load but save the result
+               }
+
+               // Loadtext is permission safe, this just clears out the diff
+               if ( !$this->loadText() ) {
+                       wfProfileOut( __METHOD__ );
+                       return false;
+               }
+
+               $difftext = $this->generateDiffBody( $this->mOldtext, $this->mNewtext );
+
+               // Save to cache for 7 days
+               if ( $key !== false && $difftext !== false ) {
+                       wfIncrStats( 'diff_cache_miss' );
+                       $wgMemc->set( $key, $difftext, 7*86400 );
+               } else {
+                       wfIncrStats( 'diff_uncacheable' );
+               }
+               // Replace line numbers with the text in the user's language
+               if ( $difftext !== false ) {
+                       $difftext = $this->localiseLineNumbers( $difftext );
+               }
+               wfProfileOut( __METHOD__ );
+               return $difftext;
+       }
+
+       /**
+        * Generate a diff, no caching
+        * $otext and $ntext must be already segmented
+        */
+       function generateDiffBody( $otext, $ntext ) {
+               global $wgExternalDiffEngine, $wgContLang;
+
+               $otext = str_replace( "\r\n", "\n", $otext );
+               $ntext = str_replace( "\r\n", "\n", $ntext );
+
+               if ( $wgExternalDiffEngine == 'wikidiff' ) {
+                       # For historical reasons, external diff engine expects
+                       # input text to be HTML-escaped already
+                       $otext = htmlspecialchars ( $wgContLang->segmentForDiff( $otext ) );
+                       $ntext = htmlspecialchars ( $wgContLang->segmentForDiff( $ntext ) );
+                       if( !function_exists( 'wikidiff_do_diff' ) ) {
+                               dl('php_wikidiff.so');
+                       }
+                       return $wgContLang->unsegementForDiff( wikidiff_do_diff( $otext, $ntext, 2 ) ) .
+                       $this->debug( 'wikidiff1' );
+               }
+
+               if ( $wgExternalDiffEngine == 'wikidiff2' ) {
+                       # Better external diff engine, the 2 may some day be dropped
+                       # This one does the escaping and segmenting itself
+                       if ( !function_exists( 'wikidiff2_do_diff' ) ) {
+                               wfProfileIn( __METHOD__ . "-dl" );
+                               @dl('php_wikidiff2.so');
+                               wfProfileOut( __METHOD__ . "-dl" );
+                       }
+                       if ( function_exists( 'wikidiff2_do_diff' ) ) {
+                               wfProfileIn( 'wikidiff2_do_diff' );
+                               $text = wikidiff2_do_diff( $otext, $ntext, 2 );
+                               $text .= $this->debug( 'wikidiff2' );
+                               wfProfileOut( 'wikidiff2_do_diff' );
+                               return $text;
+                       }
+               }
+               if ( $wgExternalDiffEngine != 'wikidiff3' && $wgExternalDiffEngine !== false ) {
+                       # Diff via the shell
+                       global $wgTmpDirectory;
+                       $tempName1 = tempnam( $wgTmpDirectory, 'diff_' );
+                       $tempName2 = tempnam( $wgTmpDirectory, 'diff_' );
+
+                       $tempFile1 = fopen( $tempName1, "w" );
+                       if ( !$tempFile1 ) {
+                               wfProfileOut( __METHOD__ );
+                               return false;
+                       }
+                       $tempFile2 = fopen( $tempName2, "w" );
+                       if ( !$tempFile2 ) {
+                               wfProfileOut( __METHOD__ );
+                               return false;
+                       }
+                       fwrite( $tempFile1, $otext );
+                       fwrite( $tempFile2, $ntext );
+                       fclose( $tempFile1 );
+                       fclose( $tempFile2 );
+                       $cmd = wfEscapeShellArg( $wgExternalDiffEngine, $tempName1, $tempName2 );
+                       wfProfileIn( __METHOD__ . "-shellexec" );
+                       $difftext = wfShellExec( $cmd );
+                       $difftext .= $this->debug( "external $wgExternalDiffEngine" );
+                       wfProfileOut( __METHOD__ . "-shellexec" );
+                       unlink( $tempName1 );
+                       unlink( $tempName2 );
+                       return $difftext;
+               }
+
+               # Native PHP diff
+               $ota = explode( "\n", $wgContLang->segmentForDiff( $otext ) );
+               $nta = explode( "\n", $wgContLang->segmentForDiff( $ntext ) );
+               $diffs = new Diff( $ota, $nta );
+               $formatter = new TableDiffFormatter();
+               return $wgContLang->unsegmentForDiff( $formatter->format( $diffs ) ) .
+               $this->debug();
+       }
+
+       /**
+        * Generate a debug comment indicating diff generating time,
+        * server node, and generator backend.
+        */
+       protected function debug( $generator="internal" ) {
+               global $wgShowHostnames, $wgNodeName;
+               $data = array( $generator );
+               if( $wgShowHostnames ) {
+                       $data[] = $wgNodeName;
+               }
+               $data[] = wfTimestamp( TS_DB );
+               return "<!-- diff generator: " .
+               implode( " ",
+               array_map(
+                                       "htmlspecialchars",
+               $data ) ) .
+                       " -->\n";
+       }
+
+       /**
+        * Replace line numbers with the text in the user's language
+        */
+       function localiseLineNumbers( $text ) {
+               return preg_replace_callback( '/<!--LINE (\d+)-->/',
+               array( &$this, 'localiseLineNumbersCb' ), $text );
+       }
+
+       function localiseLineNumbersCb( $matches ) {
+               global $wgLang;
+               return wfMsgExt( 'lineno', array('parseinline'), $wgLang->formatNum( $matches[1] ) );
+       }
+
+
+       /**
+        * If there are revisions between the ones being compared, return a note saying so.
+        */
+       function getMultiNotice() {
+               if ( !is_object($this->mOldRev) || !is_object($this->mNewRev) )
+               return '';
+
+               if( !$this->mOldPage->equals( $this->mNewPage ) ) {
+                       // Comparing two different pages? Count would be meaningless.
+                       return '';
+               }
+
+               $oldid = $this->mOldRev->getId();
+               $newid = $this->mNewRev->getId();
+               if ( $oldid > $newid ) {
+                       $tmp = $oldid; $oldid = $newid; $newid = $tmp;
+               }
+
+               $n = $this->mTitle->countRevisionsBetween( $oldid, $newid );
+               if ( !$n )
+               return '';
+
+               return wfMsgExt( 'diff-multi', array( 'parseinline' ), $n );
+       }
+
+
+       /**
+        * Add the header to a diff body
+        */
+       static function addHeader( $diff, $otitle, $ntitle, $multi = '' ) {
+               $header = "
+               <table class='diff'>
+               <col class='diff-marker' />
+               <col class='diff-content' />
+               <col class='diff-marker' />
+               <col class='diff-content' />
+               <tr valign='top'>
+               <td colspan='2' class='diff-otitle'>{$otitle}</td>
+               <td colspan='2' class='diff-ntitle'>{$ntitle}</td>
+               </tr>
+               ";
+
+               if ( $multi != '' )
+               $header .= "<tr><td colspan='4' align='center' class='diff-multi'>{$multi}</td></tr>";
+
+               return $header . $diff . "</table>";
+       }
+
+       /**
+        * Use specified text instead of loading from the database
+        */
+       function setText( $oldText, $newText ) {
+               $this->mOldtext = $oldText;
+               $this->mNewtext = $newText;
+               $this->mTextLoaded = 2;
+       }
+
+       /**
+        * Load revision metadata for the specified articles. If newid is 0, then compare
+        * the old article in oldid to the current article; if oldid is 0, then
+        * compare the current article to the immediately previous one (ignoring the
+        * value of newid).
+        *
+        * If oldid is false, leave the corresponding revision object set
+        * to false. This is impossible via ordinary user input, and is provided for
+        * API convenience.
+        */
+       function loadRevisionData() {
+               global $wgLang;
+               if ( $this->mRevisionsLoaded ) {
+                       return true;
+               } else {
+                       // Whether it succeeds or fails, we don't want to try again
+                       $this->mRevisionsLoaded = true;
+               }
+
+               // Load the new revision object
+               $this->mNewRev = $this->mNewid
+               ? Revision::newFromId( $this->mNewid )
+               : Revision::newFromTitle( $this->mTitle );
+               if( !$this->mNewRev instanceof Revision )
+               return false;
+
+               // Update the new revision ID in case it was 0 (makes life easier doing UI stuff)
+               $this->mNewid = $this->mNewRev->getId();
+
+               // Check if page is editable
+               $editable = $this->mNewRev->getTitle()->userCan( 'edit' );
+
+               // Set assorted variables
+               $timestamp = $wgLang->timeanddate( $this->mNewRev->getTimestamp(), true );
+               $this->mNewPage = $this->mNewRev->getTitle();
+               if( $this->mNewRev->isCurrent() ) {
+                       $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
+                       $this->mPagetitle = htmlspecialchars( wfMsg( 'currentrev' ) );
+                       $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit' );
+
+                       $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a> ($timestamp)";
+                       $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
+
+               } else {
+                       $newLink = $this->mNewPage->escapeLocalUrl( 'oldid=' . $this->mNewid );
+                       $newEdit = $this->mNewPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mNewid );
+                       $this->mPagetitle = wfMsgHTML( 'revisionasof', $timestamp );
+
+                       $this->mNewtitle = "<a href='$newLink'>{$this->mPagetitle}</a>";
+                       $this->mNewtitle .= " (<a href='$newEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
+               }
+               if ( !$this->mNewRev->userCan(Revision::DELETED_TEXT) ) {
+                       $this->mNewtitle = "<span class='history-deleted'>{$this->mPagetitle}</span>";
+               } else if ( $this->mNewRev->isDeleted(Revision::DELETED_TEXT) ) {
+                       $this->mNewtitle = '<span class="history-deleted">'.$this->mNewtitle.'</span>';
+               }
+
+               // Load the old revision object
+               $this->mOldRev = false;
+               if( $this->mOldid ) {
+                       $this->mOldRev = Revision::newFromId( $this->mOldid );
+               } elseif ( $this->mOldid === 0 ) {
+                       $rev = $this->mNewRev->getPrevious();
+                       if( $rev ) {
+                               $this->mOldid = $rev->getId();
+                               $this->mOldRev = $rev;
+                       } else {
+                               // No previous revision; mark to show as first-version only.
+                               $this->mOldid = false;
+                               $this->mOldRev = false;
+                       }
+               }/* elseif ( $this->mOldid === false ) leave mOldRev false; */
+
+               if( is_null( $this->mOldRev ) ) {
+                       return false;
+               }
+
+               if ( $this->mOldRev ) {
+                       $this->mOldPage = $this->mOldRev->getTitle();
+
+                       $t = $wgLang->timeanddate( $this->mOldRev->getTimestamp(), true );
+                       $oldLink = $this->mOldPage->escapeLocalUrl( 'oldid=' . $this->mOldid );
+                       $oldEdit = $this->mOldPage->escapeLocalUrl( 'action=edit&oldid=' . $this->mOldid );
+                       $this->mOldPagetitle = htmlspecialchars( wfMsg( 'revisionasof', $t ) );
+
+                       $this->mOldtitle = "<a href='$oldLink'>{$this->mOldPagetitle}</a>"
+                       . " (<a href='$oldEdit'>" . wfMsgHtml( $editable ? 'editold' : 'viewsourceold' ) . "</a>)";
+                       // Add an "undo" link
+                       $newUndo = $this->mNewPage->escapeLocalUrl( 'action=edit&undoafter=' . $this->mOldid . '&undo=' . $this->mNewid);
+                       if( $editable && !$this->mOldRev->isDeleted( Revision::DELETED_TEXT ) && !$this->mNewRev->isDeleted( Revision::DELETED_TEXT ) ) {
+                               $this->mNewtitle .= " (<a href='$newUndo'>" . htmlspecialchars( wfMsg( 'editundo' ) ) . "</a>)";
+                       }
+
+                       if( !$this->mOldRev->userCan( Revision::DELETED_TEXT ) ) {
+                               $this->mOldtitle = '<span class="history-deleted">' . $this->mOldPagetitle . '</span>';
+                       } else if( $this->mOldRev->isDeleted( Revision::DELETED_TEXT ) ) {
+                               $this->mOldtitle = '<span class="history-deleted">' . $this->mOldtitle . '</span>';
+                       }
+               }
+
+               return true;
+       }
+
+       /**
+        * Load the text of the revisions, as well as revision data.
+        */
+       function loadText() {
+               if ( $this->mTextLoaded == 2 ) {
+                       return true;
+               } else {
+                       // Whether it succeeds or fails, we don't want to try again
+                       $this->mTextLoaded = 2;
+               }
+
+               if ( !$this->loadRevisionData() ) {
+                       return false;
+               }
+               if ( $this->mOldRev ) {
+                       $this->mOldtext = $this->mOldRev->revText();
+                       if ( $this->mOldtext === false ) {
+                               return false;
+                       }
+               }
+               if ( $this->mNewRev ) {
+                       $this->mNewtext = $this->mNewRev->revText();
+                       if ( $this->mNewtext === false ) {
+                               return false;
+                       }
+               }
+               return true;
+       }
+
+       /**
+        * Load the text of the new revision, not the old one
+        */
+       function loadNewText() {
+               if ( $this->mTextLoaded >= 1 ) {
+                       return true;
+               } else {
+                       $this->mTextLoaded = 1;
+               }
+               if ( !$this->loadRevisionData() ) {
+                       return false;
+               }
+               $this->mNewtext = $this->mNewRev->getText();
+               return true;
+       }
+
+
+}
+
+// A PHP diff engine for phpwiki. (Taken from phpwiki-1.3.3)
+//
+// Copyright (C) 2000, 2001 Geoffrey T. Dairiki <dairiki@dairiki.org>
+// You may copy this code freely under the conditions of the GPL.
+//
+
+define('USE_ASSERTS', function_exists('assert'));
+
+/**
+ * @todo document
+ * @private
+ * @ingroup DifferenceEngine
+ */
+class _DiffOp {
+       var $type;
+       var $orig;
+       var $closing;
+
+       function reverse() {
+               trigger_error('pure virtual', E_USER_ERROR);
+       }
+
+       function norig() {
+               return $this->orig ? sizeof($this->orig) : 0;
+       }
+
+       function nclosing() {
+               return $this->closing ? sizeof($this->closing) : 0;
+       }
+}
+
+/**
+ * @todo document
+ * @private
+ * @ingroup DifferenceEngine
+ */
+class _DiffOp_Copy extends _DiffOp {
+       var $type = 'copy';
+
+       function _DiffOp_Copy ($orig, $closing = false) {
+               if (!is_array($closing))
+               $closing = $orig;
+               $this->orig = $orig;
+               $this->closing = $closing;
+       }
+
+       function reverse() {
+               return new _DiffOp_Copy($this->closing, $this->orig);
+       }
+}
+
+/**
+ * @todo document
+ * @private
+ * @ingroup DifferenceEngine
+ */
+class _DiffOp_Delete extends _DiffOp {
+       var $type = 'delete';
+
+       function _DiffOp_Delete ($lines) {
+               $this->orig = $lines;
+               $this->closing = false;
+       }
+
+       function reverse() {
+               return new _DiffOp_Add($this->orig);
+       }
+}
+
+/**
+ * @todo document
+ * @private
+ * @ingroup DifferenceEngine
+ */
+class _DiffOp_Add extends _DiffOp {
+       var $type = 'add';
+
+       function _DiffOp_Add ($lines) {
+               $this->closing = $lines;
+               $this->orig = false;
+       }
+
+       function reverse() {
+               return new _DiffOp_Delete($this->closing);
+       }
+}
+
+/**
+ * @todo document
+ * @private
+ * @ingroup DifferenceEngine
+ */
+class _DiffOp_Change extends _DiffOp {
+       var $type = 'change';
+
+       function _DiffOp_Change ($orig, $closing) {
+               $this->orig = $orig;
+               $this->closing = $closing;
+       }
+
+       function reverse() {
+               return new _DiffOp_Change($this->closing, $this->orig);
+       }
+}
+
+/**
+ * Class used internally by Diff to actually compute the diffs.
+ *
+ * The algorithm used here is mostly lifted from the perl module
+ * Algorithm::Diff (version 1.06) by Ned Konz, which is available at:
+ *      http://www.perl.com/CPAN/authors/id/N/NE/NEDKONZ/Algorithm-Diff-1.06.zip
+ *
+ * More ideas are taken from:
+ *      http://www.ics.uci.edu/~eppstein/161/960229.html
+ *
+ * Some ideas are (and a bit of code) are from from analyze.c, from GNU
+ * diffutils-2.7, which can be found at:
+ *      ftp://gnudist.gnu.org/pub/gnu/diffutils/diffutils-2.7.tar.gz
+ *
+ * closingly, some ideas (subdivision by NCHUNKS > 2, and some optimizations)
+ * are my own.
+ *
+ * Line length limits for robustness added by Tim Starling, 2005-08-31
+ * Alternative implementation added by Guy Van den Broeck, 2008-07-30
+ *
+ * @author Geoffrey T. Dairiki, Tim Starling, Guy Van den Broeck
+ * @private
+ * @ingroup DifferenceEngine
+ */
+class _DiffEngine {
+
+       const MAX_XREF_LENGTH =  10000;
+
+       function diff ($from_lines, $to_lines){
+               wfProfileIn( __METHOD__ );
+
+               // Diff and store locally
+               $this->diff_local($from_lines, $to_lines);
+
+               // Merge edits when possible
+               $this->_shift_boundaries($from_lines, $this->xchanged, $this->ychanged);
+               $this->_shift_boundaries($to_lines, $this->ychanged, $this->xchanged);
+
+               // Compute the edit operations.
+               $n_from = sizeof($from_lines);
+               $n_to = sizeof($to_lines);
+
+               $edits = array();
+               $xi = $yi = 0;
+               while ($xi < $n_from || $yi < $n_to) {
+                       USE_ASSERTS && assert($yi < $n_to || $this->xchanged[$xi]);
+                       USE_ASSERTS && assert($xi < $n_from || $this->ychanged[$yi]);
+
+                       // Skip matching "snake".
+                       $copy = array();
+                       while ( $xi < $n_from && $yi < $n_to
+                       && !$this->xchanged[$xi] && !$this->ychanged[$yi]) {
+                               $copy[] = $from_lines[$xi++];
+                               ++$yi;
+                       }
+                       if ($copy)
+                       $edits[] = new _DiffOp_Copy($copy);
+
+                       // Find deletes & adds.
+                       $delete = array();
+                       while ($xi < $n_from && $this->xchanged[$xi])
+                       $delete[] = $from_lines[$xi++];
+
+                       $add = array();
+                       while ($yi < $n_to && $this->ychanged[$yi])
+                       $add[] = $to_lines[$yi++];
+
+                       if ($delete && $add)
+                       $edits[] = new _DiffOp_Change($delete, $add);
+                       elseif ($delete)
+                       $edits[] = new _DiffOp_Delete($delete);
+                       elseif ($add)
+                       $edits[] = new _DiffOp_Add($add);
+               }
+               wfProfileOut( __METHOD__ );
+               return $edits;
+       }
+
+       function diff_local ($from_lines, $to_lines) {
+               global $wgExternalDiffEngine;
+               wfProfileIn( __METHOD__);
+
+               if($wgExternalDiffEngine == 'wikidiff3'){
+                       // wikidiff3
+                       $wikidiff3 = new WikiDiff3();
+                       $wikidiff3->diff($from_lines, $to_lines);
+                       $this->xchanged = $wikidiff3->removed;
+                       $this->ychanged = $wikidiff3->added;
+                       unset($wikidiff3);
+               }else{
+                       // old diff
+                       $n_from = sizeof($from_lines);
+                       $n_to = sizeof($to_lines);
+                       $this->xchanged = $this->ychanged = array();
+                       $this->xv = $this->yv = array();
+                       $this->xind = $this->yind = array();
+                       unset($this->seq);
+                       unset($this->in_seq);
+                       unset($this->lcs);
+
+                       // Skip leading common lines.
+                       for ($skip = 0; $skip < $n_from && $skip < $n_to; $skip++) {
+                               if ($from_lines[$skip] !== $to_lines[$skip])
+                               break;
+                               $this->xchanged[$skip] = $this->ychanged[$skip] = false;
+                       }
+                       // Skip trailing common lines.
+                       $xi = $n_from; $yi = $n_to;
+                       for ($endskip = 0; --$xi > $skip && --$yi > $skip; $endskip++) {
+                               if ($from_lines[$xi] !== $to_lines[$yi])
+                               break;
+                               $this->xchanged[$xi] = $this->ychanged[$yi] = false;
+                       }
+
+                       // Ignore lines which do not exist in both files.
+                       for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+                               $xhash[$this->_line_hash($from_lines[$xi])] = 1;
+                       }
+
+                       for ($yi = $skip; $yi < $n_to - $endskip; $yi++) {
+                               $line = $to_lines[$yi];
+                               if ( ($this->ychanged[$yi] = empty($xhash[$this->_line_hash($line)])) )
+                               continue;
+                               $yhash[$this->_line_hash($line)] = 1;
+                               $this->yv[] = $line;
+                               $this->yind[] = $yi;
+                       }
+                       for ($xi = $skip; $xi < $n_from - $endskip; $xi++) {
+                               $line = $from_lines[$xi];
+                               if ( ($this->xchanged[$xi] = empty($yhash[$this->_line_hash($line)])) )
+                               continue;
+                               $this->xv[] = $line;
+                               $this->xind[] = $xi;
+                       }
+
+                       // Find the LCS.
+                       $this->_compareseq(0, sizeof($this->xv), 0, sizeof($this->yv));
+               }
+               wfProfileOut( __METHOD__ );
+       }
+
+       /**
+        * Returns the whole line if it's small enough, or the MD5 hash otherwise
+        */
+       function _line_hash( $line ) {
+               if ( strlen( $line ) > self::MAX_XREF_LENGTH ) {
+                       return md5( $line );
+               } else {
+                       return $line;
+               }
+       }
+
+       /* Divide the Largest Common Subsequence (LCS) of the sequences
+        * [XOFF, XLIM) and [YOFF, YLIM) into NCHUNKS approximately equally
+        * sized segments.
+        *
+        * Returns (LCS, PTS).  LCS is the length of the LCS. PTS is an
+        * array of NCHUNKS+1 (X, Y) indexes giving the diving points between
+        * sub sequences.  The first sub-sequence is contained in [X0, X1),
+        * [Y0, Y1), the second in [X1, X2), [Y1, Y2) and so on.  Note
+        * that (X0, Y0) == (XOFF, YOFF) and
+        * (X[NCHUNKS], Y[NCHUNKS]) == (XLIM, YLIM).
+        *
+        * This function assumes that the first lines of the specified portions
+        * of the two files do not match, and likewise that the last lines do not
+        * match.  The caller must trim matching lines from the beginning and end
+        * of the portions it is going to specify.
+        */
+       function _diag ($xoff, $xlim, $yoff, $ylim, $nchunks) {
+               $flip = false;
+
+               if ($xlim - $xoff > $ylim - $yoff) {
+                       // Things seems faster (I'm not sure I understand why)
+                       // when the shortest sequence in X.
+                       $flip = true;
+                       list ($xoff, $xlim, $yoff, $ylim)
+                       = array( $yoff, $ylim, $xoff, $xlim);
+               }
+
+               if ($flip)
+               for ($i = $ylim - 1; $i >= $yoff; $i--)
+               $ymatches[$this->xv[$i]][] = $i;
+               else
+               for ($i = $ylim - 1; $i >= $yoff; $i--)
+               $ymatches[$this->yv[$i]][] = $i;
+
+               $this->lcs = 0;
+               $this->seq[0]= $yoff - 1;
+               $this->in_seq = array();
+               $ymids[0] = array();
+
+               $numer = $xlim - $xoff + $nchunks - 1;
+               $x = $xoff;
+               for ($chunk = 0; $chunk < $nchunks; $chunk++) {
+                       if ($chunk > 0)
+                       for ($i = 0; $i <= $this->lcs; $i++)
+                       $ymids[$i][$chunk-1] = $this->seq[$i];
+
+                       $x1 = $xoff + (int)(($numer + ($xlim-$xoff)*$chunk) / $nchunks);
+                       for ( ; $x < $x1; $x++) {
+                               $line = $flip ? $this->yv[$x] : $this->xv[$x];
+                               if (empty($ymatches[$line]))
+                               continue;
+                               $matches = $ymatches[$line];
+                               reset($matches);
+                               while (list ($junk, $y) = each($matches))
+                               if (empty($this->in_seq[$y])) {
+                                       $k = $this->_lcs_pos($y);
+                                       USE_ASSERTS && assert($k > 0);
+                                       $ymids[$k] = $ymids[$k-1];
+                                       break;
+                               }
+                               while (list ( /* $junk */, $y) = each($matches)) {
+                                       if ($y > $this->seq[$k-1]) {
+                                               USE_ASSERTS && assert($y < $this->seq[$k]);
+                                               // Optimization: this is a common case:
+                                               //      next match is just replacing previous match.
+                                               $this->in_seq[$this->seq[$k]] = false;
+                                               $this->seq[$k] = $y;
+                                               $this->in_seq[$y] = 1;
+                                       } else if (empty($this->in_seq[$y])) {
+                                               $k = $this->_lcs_pos($y);
+                                               USE_ASSERTS && assert($k > 0);
+                                               $ymids[$k] = $ymids[$k-1];
+                                       }
+                               }
+                       }
+               }
+
+               $seps[] = $flip ? array($yoff, $xoff) : array($xoff, $yoff);
+               $ymid = $ymids[$this->lcs];
+               for ($n = 0; $n < $nchunks - 1; $n++) {
+                       $x1 = $xoff + (int)(($numer + ($xlim - $xoff) * $n) / $nchunks);
+                       $y1 = $ymid[$n] + 1;
+                       $seps[] = $flip ? array($y1, $x1) : array($x1, $y1);
+               }
+               $seps[] = $flip ? array($ylim, $xlim) : array($xlim, $ylim);
+
+               return array($this->lcs, $seps);
+       }
+
+       function _lcs_pos ($ypos) {
+               $end = $this->lcs;
+               if ($end == 0 || $ypos > $this->seq[$end]) {
+                       $this->seq[++$this->lcs] = $ypos;
+                       $this->in_seq[$ypos] = 1;
+                       return $this->lcs;
+               }
+
+               $beg = 1;
+               while ($beg < $end) {
+                       $mid = (int)(($beg + $end) / 2);
+                       if ( $ypos > $this->seq[$mid] )
+                       $beg = $mid + 1;
+                       else
+                       $end = $mid;
+               }
+
+               USE_ASSERTS && assert($ypos != $this->seq[$end]);
+
+               $this->in_seq[$this->seq[$end]] = false;
+               $this->seq[$end] = $ypos;
+               $this->in_seq[$ypos] = 1;
+               return $end;
+       }
+
+       /* Find LCS of two sequences.
+        *
+        * The results are recorded in the vectors $this->{x,y}changed[], by
+        * storing a 1 in the element for each line that is an insertion
+        * or deletion (ie. is not in the LCS).
+        *
+        * The subsequence of file 0 is [XOFF, XLIM) and likewise for file 1.
+        *
+        * Note that XLIM, YLIM are exclusive bounds.
+        * All line numbers are origin-0 and discarded lines are not counted.
+        */
+       function _compareseq ($xoff, $xlim, $yoff, $ylim) {
+               // Slide down the bottom initial diagonal.
+               while ($xoff < $xlim && $yoff < $ylim
+               && $this->xv[$xoff] == $this->yv[$yoff]) {
+                       ++$xoff;
+                       ++$yoff;
+               }
+
+               // Slide up the top initial diagonal.
+               while ($xlim > $xoff && $ylim > $yoff
+               && $this->xv[$xlim - 1] == $this->yv[$ylim - 1]) {
+                       --$xlim;
+                       --$ylim;
+               }
+
+               if ($xoff == $xlim || $yoff == $ylim)
+               $lcs = 0;
+               else {
+                       // This is ad hoc but seems to work well.
+                       //$nchunks = sqrt(min($xlim - $xoff, $ylim - $yoff) / 2.5);
+                       //$nchunks = max(2,min(8,(int)$nchunks));
+                       $nchunks = min(7, $xlim - $xoff, $ylim - $yoff) + 1;
+                       list ($lcs, $seps)
+                       = $this->_diag($xoff,$xlim,$yoff, $ylim,$nchunks);
+               }
+
+               if ($lcs == 0) {
+                       // X and Y sequences have no common subsequence:
+                       // mark all changed.
+                       while ($yoff < $ylim)
+                       $this->ychanged[$this->yind[$yoff++]] = 1;
+                       while ($xoff < $xlim)
+                       $this->xchanged[$this->xind[$xoff++]] = 1;
+               } else {
+                       // Use the partitions to split this problem into subproblems.
+                       reset($seps);
+                       $pt1 = $seps[0];
+                       while ($pt2 = next($seps)) {
+                               $this->_compareseq ($pt1[0], $pt2[0], $pt1[1], $pt2[1]);
+                               $pt1 = $pt2;
+                       }
+               }
+       }
+
+       /* Adjust inserts/deletes of identical lines to join changes
+        * as much as possible.
+        *
+        * We do something when a run of changed lines include a
+        * line at one end and has an excluded, identical line at the other.
+        * We are free to choose which identical line is included.
+        * `compareseq' usually chooses the one at the beginning,
+        * but usually it is cleaner to consider the following identical line
+        * to be the "change".
+        *
+        * This is extracted verbatim from analyze.c (GNU diffutils-2.7).
+        */
+       function _shift_boundaries ($lines, &$changed, $other_changed) {
+               wfProfileIn( __METHOD__ );
+               $i = 0;
+               $j = 0;
+
+               USE_ASSERTS && assert('sizeof($lines) == sizeof($changed)');
+               $len = sizeof($lines);
+               $other_len = sizeof($other_changed);
+
+               while (1) {
+                       /*
+                        * Scan forwards to find beginning of another run of changes.
+                        * Also keep track of the corresponding point in the other file.
+                        *
+                        * Throughout this code, $i and $j are adjusted together so that
+                        * the first $i elements of $changed and the first $j elements
+                        * of $other_changed both contain the same number of zeros
+                        * (unchanged lines).
+                        * Furthermore, $j is always kept so that $j == $other_len or
+                        * $other_changed[$j] == false.
+                        */
+                       while ($j < $other_len && $other_changed[$j])
+                       $j++;
+
+                       while ($i < $len && ! $changed[$i]) {
+                               USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+                               $i++; $j++;
+                               while ($j < $other_len && $other_changed[$j])
+                               $j++;
+                       }
+
+                       if ($i == $len)
+                       break;
+
+                       $start = $i;
+
+                       // Find the end of this run of changes.
+                       while (++$i < $len && $changed[$i])
+                       continue;
+
+                       do {
+                               /*
+                                * Record the length of this run of changes, so that
+                                * we can later determine whether the run has grown.
+                                */
+                               $runlength = $i - $start;
+
+                               /*
+                                * Move the changed region back, so long as the
+                                * previous unchanged line matches the last changed one.
+                                * This merges with previous changed regions.
+                                */
+                               while ($start > 0 && $lines[$start - 1] == $lines[$i - 1]) {
+                                       $changed[--$start] = 1;
+                                       $changed[--$i] = false;
+                                       while ($start > 0 && $changed[$start - 1])
+                                       $start--;
+                                       USE_ASSERTS && assert('$j > 0');
+                                       while ($other_changed[--$j])
+                                       continue;
+                                       USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+                               }
+
+                               /*
+                                * Set CORRESPONDING to the end of the changed run, at the last
+                                * point where it corresponds to a changed run in the other file.
+                                * CORRESPONDING == LEN means no such point has been found.
+                                */
+                               $corresponding = $j < $other_len ? $i : $len;
+
+                               /*
+                                * Move the changed region forward, so long as the
+                                * first changed line matches the following unchanged one.
+                                * This merges with following changed regions.
+                                * Do this second, so that if there are no merges,
+                                * the changed region is moved forward as far as possible.
+                                */
+                               while ($i < $len && $lines[$start] == $lines[$i]) {
+                                       $changed[$start++] = false;
+                                       $changed[$i++] = 1;
+                                       while ($i < $len && $changed[$i])
+                                       $i++;
+
+                                       USE_ASSERTS && assert('$j < $other_len && ! $other_changed[$j]');
+                                       $j++;
+                                       if ($j < $other_len && $other_changed[$j]) {
+                                               $corresponding = $i;
+                                               while ($j < $other_len && $other_changed[$j])
+                                               $j++;
+                                       }
+                               }
+                       } while ($runlength != $i - $start);
+
+                       /*
+                        * If possible, move the fully-merged run of changes
+                        * back to a corresponding run in the other file.
+                        */
+                       while ($corresponding < $i) {
+                               $changed[--$start] = 1;
+                               $changed[--$i] = 0;
+                               USE_ASSERTS && assert('$j > 0');
+                               while ($other_changed[--$j])
+                               continue;
+                               USE_ASSERTS && assert('$j >= 0 && !$other_changed[$j]');
+                       }
+               }
+               wfProfileOut( __METHOD__ );
+       }
+}
+
+/**
+ * Class representing a 'diff' between two sequences of strings.
+ * @todo document
+ * @private
+ * @ingroup DifferenceEngine
+ */
+class Diff
+{
+       var $edits;
+
+       /**
+        * Constructor.
+        * Computes diff between sequences of strings.
+        *
+        * @param $from_lines array An array of strings.
+        *                (Typically these are lines from a file.)
+        * @param $to_lines array An array of strings.
+        */
+       function Diff($from_lines, $to_lines) {
+               $eng = new _DiffEngine;
+               $this->edits = $eng->diff($from_lines, $to_lines);
+               //$this->_check($from_lines, $to_lines);
+       }
+
+       /**
+        * Compute reversed Diff.
+        *
+        * SYNOPSIS:
+        *
+        *      $diff = new Diff($lines1, $lines2);
+        *      $rev = $diff->reverse();
+        * @return object A Diff object representing the inverse of the
+        *                                original diff.
+        */
+       function reverse () {
+               $rev = $this;
+               $rev->edits = array();
+               foreach ($this->edits as $edit) {
+                       $rev->edits[] = $edit->reverse();
+               }
+               return $rev;
+       }
+
+       /**
+        * Check for empty diff.
+        *
+        * @return bool True iff two sequences were identical.
+        */
+       function isEmpty () {
+               foreach ($this->edits as $edit) {
+                       if ($edit->type != 'copy')
+                       return false;
+               }
+               return true;
+       }
+
+       /**
+        * Compute the length of the Longest Common Subsequence (LCS).
+        *
+        * This is mostly for diagnostic purposed.
+        *
+        * @return int The length of the LCS.
+        */
+       function lcs () {
+               $lcs = 0;
+               foreach ($this->edits as $edit) {
+                       if ($edit->type == 'copy')
+                       $lcs += sizeof($edit->orig);
+               }
+               return $lcs;
+       }
+
+       /**
+        * Get the original set of lines.
+        *
+        * This reconstructs the $from_lines parameter passed to the
+        * constructor.
+        *
+        * @return array The original sequence of strings.
+        */
+       function orig() {
+               $lines = array();
+
+               foreach ($this->edits as $edit) {
+                       if ($edit->orig)
+                       array_splice($lines, sizeof($lines), 0, $edit->orig);
+               }
+               return $lines;
+       }
+
+       /**
+        * Get the closing set of lines.
+        *
+        * This reconstructs the $to_lines parameter passed to the
+        * constructor.
+        *
+        * @return array The sequence of strings.
+        */
+       function closing() {
+               $lines = array();
+
+               foreach ($this->edits as $edit) {
+                       if ($edit->closing)
+                       array_splice($lines, sizeof($lines), 0, $edit->closing);
+               }
+               return $lines;
+       }
+
+       /**
+        * Check a Diff for validity.
+        *
+        * This is here only for debugging purposes.
+        */
+       function _check ($from_lines, $to_lines) {
+               wfProfileIn( __METHOD__ );
+               if (serialize($from_lines) != serialize($this->orig()))
+               trigger_error("Reconstructed original doesn't match", E_USER_ERROR);
+               if (serialize($to_lines) != serialize($this->closing()))
+               trigger_error("Reconstructed closing doesn't match", E_USER_ERROR);
+
+               $rev = $this->reverse();
+               if (serialize($to_lines) != serialize($rev->orig()))
+               trigger_error("Reversed original doesn't match", E_USER_ERROR);
+               if (serialize($from_lines) != serialize($rev->closing()))
+               trigger_error("Reversed closing doesn't match", E_USER_ERROR);
+
+
+               $prevtype = 'none';
+               foreach ($this->edits as $edit) {
+                       if ( $prevtype == $edit->type )
+                       trigger_error("Edit sequence is non-optimal", E_USER_ERROR);
+                       $prevtype = $edit->type;
+               }
+
+               $lcs = $this->lcs();
+               trigger_error('Diff okay: LCS = '.$lcs, E_USER_NOTICE);
+               wfProfileOut( __METHOD__ );
+       }
+}
+
+/**
+ * @todo document, bad name.
+ * @private
+ * @ingroup DifferenceEngine
+ */
+class MappedDiff extends Diff
+{
+       /**
+        * Constructor.
+        *
+        * Computes diff between sequences of strings.
+        *
+        * This can be used to compute things like
+        * case-insensitve diffs, or diffs which ignore
+        * changes in white-space.
+        *
+        * @param $from_lines array An array of strings.
+        *      (Typically these are lines from a file.)
+        *
+        * @param $to_lines array An array of strings.
+        *
+        * @param $mapped_from_lines array This array should
+        *      have the same size number of elements as $from_lines.
+        *      The elements in $mapped_from_lines and
+        *      $mapped_to_lines are what is actually compared
+        *      when computing the diff.
+        *
+        * @param $mapped_to_lines array This array should
+        *      have the same number of elements as $to_lines.
+        */
+       function MappedDiff($from_lines, $to_lines,
+       $mapped_from_lines, $mapped_to_lines) {
+               wfProfileIn( __METHOD__ );
+
+               assert(sizeof($from_lines) == sizeof($mapped_from_lines));
+               assert(sizeof($to_lines) == sizeof($mapped_to_lines));
+
+               $this->Diff($mapped_from_lines, $mapped_to_lines);
+
+               $xi = $yi = 0;
+               for ($i = 0; $i < sizeof($this->edits); $i++) {
+                       $orig = &$this->edits[$i]->orig;
+                       if (is_array($orig)) {
+                               $orig = array_slice($from_lines, $xi, sizeof($orig));
+                               $xi += sizeof($orig);
+                       }
+
+                       $closing = &$this->edits[$i]->closing;
+                       if (is_array($closing)) {
+                               $closing = array_slice($to_lines, $yi, sizeof($closing));
+                               $yi += sizeof($closing);
+                       }
+               }
+               wfProfileOut( __METHOD__ );
+       }
+}
+
+/**
+ * A class to format Diffs
+ *
+ * This class formats the diff in classic diff format.
+ * It is intended that this class be customized via inheritance,
+ * to obtain fancier outputs.
+ * @todo document
+ * @private
+ * @ingroup DifferenceEngine
+ */
+class DiffFormatter {
+       /**
+        * Number of leading context "lines" to preserve.
+        *
+        * This should be left at zero for this class, but subclasses
+        * may want to set this to other values.
+        */
+       var $leading_context_lines = 0;
+
+       /**
+        * Number of trailing context "lines" to preserve.
+        *
+        * This should be left at zero for this class, but subclasses
+        * may want to set this to other values.
+        */
+       var $trailing_context_lines = 0;
+
+       /**
+        * Format a diff.
+        *
+        * @param $diff object A Diff object.
+        * @return string The formatted output.
+        */
+       function format($diff) {
+               wfProfileIn( __METHOD__ );
+
+               $xi = $yi = 1;
+               $block = false;
+               $context = array();
+
+               $nlead = $this->leading_context_lines;
+               $ntrail = $this->trailing_context_lines;
+
+               $this->_start_diff();
+
+               foreach ($diff->edits as $edit) {
+                       if ($edit->type == 'copy') {
+                               if (is_array($block)) {
+                                       if (sizeof($edit->orig) <= $nlead + $ntrail) {
+                                               $block[] = $edit;
+                                       }
+                                       else{
+                                               if ($ntrail) {
+                                                       $context = array_slice($edit->orig, 0, $ntrail);
+                                                       $block[] = new _DiffOp_Copy($context);
+                                               }
+                                               $this->_block($x0, $ntrail + $xi - $x0,
+                                               $y0, $ntrail + $yi - $y0,
+                                               $block);
+                                               $block = false;
+                                       }
+                               }
+                               $context = $edit->orig;
+                       }
+                       else {
+                               if (! is_array($block)) {
+                                       $context = array_slice($context, sizeof($context) - $nlead);
+                                       $x0 = $xi - sizeof($context);
+                                       $y0 = $yi - sizeof($context);
+                                       $block = array();
+                                       if ($context)
+                                       $block[] = new _DiffOp_Copy($context);
+                               }
+                               $block[] = $edit;
+                       }
+
+                       if ($edit->orig)
+                       $xi += sizeof($edit->orig);
+                       if ($edit->closing)
+                       $yi += sizeof($edit->closing);
+               }
+
+               if (is_array($block))
+               $this->_block($x0, $xi - $x0,
+               $y0, $yi - $y0,
+               $block);
+
+               $end = $this->_end_diff();
+               wfProfileOut( __METHOD__ );
+               return $end;
+       }
+
+       function _block($xbeg, $xlen, $ybeg, $ylen, &$edits) {
+               wfProfileIn( __METHOD__ );
+               $this->_start_block($this->_block_header($xbeg, $xlen, $ybeg, $ylen));
+               foreach ($edits as $edit) {
+                       if ($edit->type == 'copy')
+                       $this->_context($edit->orig);
+                       elseif ($edit->type == 'add')
+                       $this->_added($edit->closing);
+                       elseif ($edit->type == 'delete')
+                       $this->_deleted($edit->orig);
+                       elseif ($edit->type == 'change')
+                       $this->_changed($edit->orig, $edit->closing);
+                       else
+                       trigger_error('Unknown edit type', E_USER_ERROR);
+               }
+               $this->_end_block();
+               wfProfileOut( __METHOD__ );
+       }
+
+       function _start_diff() {
+               ob_start();
+       }
+
+       function _end_diff() {
+               $val = ob_get_contents();
+               ob_end_clean();
+               return $val;
+       }
+
+       function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+               if ($xlen > 1)
+               $xbeg .= "," . ($xbeg + $xlen - 1);
+               if ($ylen > 1)
+               $ybeg .= "," . ($ybeg + $ylen - 1);
+
+               return $xbeg . ($xlen ? ($ylen ? 'c' : 'd') : 'a') . $ybeg;
+       }
+
+       function _start_block($header) {
+               echo $header . "\n";
+       }
+
+       function _end_block() {
+       }
+
+       function _lines($lines, $prefix = ' ') {
+               foreach ($lines as $line)
+               echo "$prefix $line\n";
+       }
+
+       function _context($lines) {
+               $this->_lines($lines);
+       }
+
+       function _added($lines) {
+               $this->_lines($lines, '>');
+       }
+       function _deleted($lines) {
+               $this->_lines($lines, '<');
+       }
+
+       function _changed($orig, $closing) {
+               $this->_deleted($orig);
+               echo "---\n";
+               $this->_added($closing);
+       }
+}
+
+/**
+ * A formatter that outputs unified diffs
+ * @ingroup DifferenceEngine
+ */
+
+class UnifiedDiffFormatter extends DiffFormatter {
+       var $leading_context_lines = 2;
+       var $trailing_context_lines = 2;
+
+       function _added($lines) {
+               $this->_lines($lines, '+');
+       }
+       function _deleted($lines) {
+               $this->_lines($lines, '-');
+       }
+       function _changed($orig, $closing) {
+               $this->_deleted($orig);
+               $this->_added($closing);
+       }
+       function _block_header($xbeg, $xlen, $ybeg, $ylen) {
+               return "@@ -$xbeg,$xlen +$ybeg,$ylen @@";
+       }
+}
+
+/**
+ * A pseudo-formatter that just passes along the Diff::$edits array
+ * @ingroup DifferenceEngine
+ */
+class ArrayDiffFormatter extends DiffFormatter {
+       function format($diff) {
+               $oldline = 1;
+               $newline = 1;
+               $retval = array();
+               foreach($diff->edits as $edit)
+               switch($edit->type) {
+                       case 'add':
+                               foreach($edit->closing as $l) {
+                                       $retval[] = array(
+                                                       'action' => 'add',
+                                                       'new'=> $l,
+                                                       'newline' => $newline++
+                                       );
+                               }
+                               break;
+                       case 'delete':
+                               foreach($edit->orig as $l) {
+                                       $retval[] = array(
+                                                       'action' => 'delete',
+                                                       'old' => $l,
+                                                       'oldline' => $oldline++,
+                                       );
+                               }
+                               break;
+                       case 'change':
+                               foreach($edit->orig as $i => $l) {
+                                       $retval[] = array(
+                                                       'action' => 'change',
+                                                       'old' => $l,
+                                                       'new' => @$edit->closing[$i],
+                                                       'oldline' => $oldline++,
+                                                       'newline' => $newline++,
+                                       );
+                               }
+                               break;
+                       case 'copy':
+                               $oldline += count($edit->orig);
+                               $newline += count($edit->orig);
+               }
+               return $retval;
+       }
+}
+
+/**
+ *     Additions by Axel Boldt follow, partly taken from diff.php, phpwiki-1.3.3
+ *
+ */
+
+define('NBSP', '&#160;'); // iso-8859-x non-breaking space.
+
+/**
+ * @todo document
+ * @private
+ * @ingroup DifferenceEngine
+ */
+class _HWLDF_WordAccumulator {
+       function _HWLDF_WordAccumulator () {
+               $this->_lines = array();
+               $this->_line = '';
+               $this->_group = '';
+               $this->_tag = '';
+       }
+
+       function _flushGroup ($new_tag) {
+               if ($this->_group !== '') {
+                       if ($this->_tag == 'ins')
+                       $this->_line .= '<ins class="diffchange diffchange-inline">' .
+                       htmlspecialchars ( $this->_group ) . '</ins>';
+                       elseif ($this->_tag == 'del')
+                       $this->_line .= '<del class="diffchange diffchange-inline">' .
+                       htmlspecialchars ( $this->_group ) . '</del>';
+                       else
+                       $this->_line .= htmlspecialchars ( $this->_group );
+               }
+               $this->_group = '';
+               $this->_tag = $new_tag;
+       }
+
+       function _flushLine ($new_tag) {
+               $this->_flushGroup($new_tag);
+               if ($this->_line != '')
+               array_push ( $this->_lines, $this->_line );
+               else
+               # make empty lines visible by inserting an NBSP
+               array_push ( $this->_lines, NBSP );
+               $this->_line = '';
+       }
+
+       function addWords ($words, $tag = '') {
+               if ($tag != $this->_tag)
+               $this->_flushGroup($tag);
+
+               foreach ($words as $word) {
+                       // new-line should only come as first char of word.
+                       if ($word == '')
+                       continue;
+                       if ($word[0] == "\n") {
+                               $this->_flushLine($tag);
+                               $word = substr($word, 1);
+                       }
+                       assert(!strstr($word, "\n"));
+                       $this->_group .= $word;
+               }
+       }
+
+       function getLines() {
+               $this->_flushLine('~done');
+               return $this->_lines;
+       }
+}
+
+/**
+ * @todo document
+ * @private
+ * @ingroup DifferenceEngine
+ */
+class WordLevelDiff extends MappedDiff {
+       const MAX_LINE_LENGTH = 10000;
+
+       function WordLevelDiff ($orig_lines, $closing_lines) {
+               wfProfileIn( __METHOD__ );
+
+               list ($orig_words, $orig_stripped) = $this->_split($orig_lines);
+               list ($closing_words, $closing_stripped) = $this->_split($closing_lines);
+
+               $this->MappedDiff($orig_words, $closing_words,
+               $orig_stripped, $closing_stripped);
+               wfProfileOut( __METHOD__ );
+       }
+
+       function _split($lines) {
+               wfProfileIn( __METHOD__ );
+
+               $words = array();
+               $stripped = array();
+               $first = true;
+               foreach ( $lines as $line ) {
+                       # If the line is too long, just pretend the entire line is one big word
+                       # This prevents resource exhaustion problems
+                       if ( $first ) {
+                               $first = false;
+                       } else {
+                               $words[] = "\n";
+                               $stripped[] = "\n";
+                       }
+                       if ( strlen( $line ) > self::MAX_LINE_LENGTH ) {
+                               $words[] = $line;
+                               $stripped[] = $line;
+                       } else {
+                               $m = array();
+                               if (preg_match_all('/ ( [^\S\n]+ | [0-9_A-Za-z\x80-\xff]+ | . ) (?: (?!< \n) [^\S\n])? /xs',
+                               $line, $m))
+                               {
+                                       $words = array_merge( $words, $m[0] );
+                                       $stripped = array_merge( $stripped, $m[1] );
+                               }
+                       }
+               }
+               wfProfileOut( __METHOD__ );
+               return array($words, $stripped);
+       }
+
+       function orig () {
+               wfProfileIn( __METHOD__ );
+               $orig = new _HWLDF_WordAccumulator;
+
+               foreach ($this->edits as $edit) {
+                       if ($edit->type == 'copy')
+                       $orig->addWords($edit->orig);
+                       elseif ($edit->orig)
+                       $orig->addWords($edit->orig, 'del');
+               }
+               $lines = $orig->getLines();
+               wfProfileOut( __METHOD__ );
+               return $lines;
+       }
+
+       function closing () {
+               wfProfileIn( __METHOD__ );
+               $closing = new _HWLDF_WordAccumulator;
+
+               foreach ($this->edits as $edit) {
+                       if ($edit->type == 'copy')
+                       $closing->addWords($edit->closing);
+                       elseif ($edit->closing)
+                       $closing->addWords($edit->closing, 'ins');
+               }
+               $lines = $closing->getLines();
+               wfProfileOut( __METHOD__ );
+               return $lines;
+       }
+}
+
+/**
+ * Wikipedia Table style diff formatter.
+ * @todo document
+ * @private
+ * @ingroup DifferenceEngine
+ */
+class TableDiffFormatter extends DiffFormatter {
+       function TableDiffFormatter() {
+               $this->leading_context_lines = 2;
+               $this->trailing_context_lines = 2;
+       }
+
+       public static function escapeWhiteSpace( $msg ) {
+               $msg = preg_replace( '/^ /m', '&nbsp; ', $msg );
+               $msg = preg_replace( '/ $/m', ' &nbsp;', $msg );
+               $msg = preg_replace( '/  /', '&nbsp; ', $msg );
+               return $msg;
+       }
+
+       function _block_header( $xbeg, $xlen, $ybeg, $ylen ) {
+               $r = '<tr><td colspan="2" class="diff-lineno"><!--LINE '.$xbeg."--></td>\n" .
+                 '<td colspan="2" class="diff-lineno"><!--LINE '.$ybeg."--></td></tr>\n";
+               return $r;
+       }
+
+       function _start_block( $header ) {
+               echo $header;
+       }
+
+       function _end_block() {
+       }
+
+       function _lines( $lines, $prefix=' ', $color='white' ) {
+       }
+
+       # HTML-escape parameter before calling this
+       function addedLine( $line ) {
+               return $this->wrapLine( '+', 'diff-addedline', $line );
+       }
+
+       # HTML-escape parameter before calling this
+       function deletedLine( $line ) {
+               return $this->wrapLine( '-', 'diff-deletedline', $line );
+       }
+
+       # HTML-escape parameter before calling this
+       function contextLine( $line ) {
+               return $this->wrapLine( ' ', 'diff-context', $line );
+       }
+
+       private function wrapLine( $marker, $class, $line ) {
+               if( $line !== '' ) {
+                       // The <div> wrapper is needed for 'overflow: auto' style to scroll properly
+                       $line = Xml::tags( 'div', null, $this->escapeWhiteSpace( $line ) );
+               }
+               return "<td class='diff-marker'>$marker</td><td class='$class'>$line</td>";
+       }
+
+       function emptyLine() {
+               return '<td colspan="2">&nbsp;</td>';
+       }
+
+       function _added( $lines ) {
+               foreach ($lines as $line) {
+                       echo '<tr>' . $this->emptyLine() .
+                       $this->addedLine( '<ins class="diffchange">' .
+                       htmlspecialchars ( $line ) . '</ins>' ) . "</tr>\n";
+               }
+       }
+
+       function _deleted($lines) {
+               foreach ($lines as $line) {
+                       echo '<tr>' . $this->deletedLine( '<del class="diffchange">' .
+                       htmlspecialchars ( $line ) . '</del>' ) .
+                       $this->emptyLine() . "</tr>\n";
+               }
+       }
+
+       function _context( $lines ) {
+               foreach ($lines as $line) {
+                       echo '<tr>' .
+                       $this->contextLine( htmlspecialchars ( $line ) ) .
+                       $this->contextLine( htmlspecialchars ( $line ) ) . "</tr>\n";
+               }
+       }
+
+       function _changed( $orig, $closing ) {
+               wfProfileIn( __METHOD__ );
+
+               $diff = new WordLevelDiff( $orig, $closing );
+               $del = $diff->orig();
+               $add = $diff->closing();
+
+               # Notice that WordLevelDiff returns HTML-escaped output.
+               # Hence, we will be calling addedLine/deletedLine without HTML-escaping.
+
+               while ( $line = array_shift( $del ) ) {
+                       $aline = array_shift( $add );
+                       echo '<tr>' . $this->deletedLine( $line ) .
+                       $this->addedLine( $aline ) . "</tr>\n";
+               }
+               foreach ($add as $line) {       # If any leftovers
+                       echo '<tr>' . $this->emptyLine() .
+                       $this->addedLine( $line ) . "</tr>\n";
+               }
+               wfProfileOut( __METHOD__ );
+       }
+}
\ No newline at end of file
diff --git a/includes/diff/HTMLDiff.php b/includes/diff/HTMLDiff.php
new file mode 100644 (file)
index 0000000..8a162c1
--- /dev/null
@@ -0,0 +1,1036 @@
+<?php
+
+/** Copyright (C) 2008 Guy Van den Broeck <guy@guyvdb.eu>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * or see http://www.gnu.org/
+ * 
+ * @ingroup DifferenceEngine
+ */
+
+/**
+ * When detecting the last common parent of two nodes, all results are stored as
+ * a LastCommonParentResult.
+ */
+class LastCommonParentResult {
+
+       // Parent
+       public $parent;
+
+       // Splitting
+       public $splittingNeeded = false;
+
+       // Depth
+       public $lastCommonParentDepth = -1;
+
+       // Index
+       public $indexInLastCommonParent = -1;
+}
+
+class Modification{
+
+       const NONE = 1;
+       const REMOVED = 2;
+       const ADDED = 4;
+       const CHANGED = 8;
+
+       public $type;
+
+       public $id = -1;
+
+       public $firstOfID = false;
+
+       public $changes;
+
+       function __construct($type) {
+               $this->type = $type;
+       }
+
+       public static function typeToString($type) {
+               switch($type) {
+                       case self::NONE: return 'none';
+                       case self::REMOVED: return 'removed';
+                       case self::ADDED: return 'added';
+                       case self::CHANGED: return 'changed';
+               }
+       }
+}
+
+class DomTreeBuilder {
+
+       public $textNodes = array();
+
+       public $bodyNode;
+
+       private $currentParent;
+
+       private $newWord = '';
+
+       protected $bodyStarted = false;
+
+       protected $bodyEnded = false;
+
+       private $whiteSpaceBeforeThis = false;
+
+       private $lastSibling;
+
+       private $notInPre = true;
+
+       function __construct() {
+               $this->bodyNode = $this->currentParent = new BodyNode();
+               $this->lastSibling = new DummyNode();
+       }
+
+       /**
+        * Must be called manually
+        */
+       public function endDocument() {
+               $this->endWord();
+               HTMLDiffer::diffDebug( count($this->textNodes) . " text nodes in document.\n" );
+       }
+
+       public function startElement($parser, $name, /*array*/ $attributes) {
+               if (strcasecmp($name, 'body') != 0) {
+                       HTMLDiffer::diffDebug( "Starting $name node.\n" );
+                       $this->endWord();
+
+                       $newNode = new TagNode($this->currentParent, $name, $attributes);
+                       $this->currentParent->children[] = $newNode;
+                       $this->currentParent = $newNode;
+                       $this->lastSibling = new DummyNode();
+                       if ($this->whiteSpaceBeforeThis && !in_array(strtolower($this->currentParent->qName),TagNode::$blocks)) {
+                               $this->currentParent->whiteBefore = true;
+                       }
+                       $this->whiteSpaceBeforeThis = false;
+                       if(strcasecmp($name, 'pre') == 0) {
+                               $this->notInPre = false;
+                       }
+               }
+       }
+
+       public function endElement($parser, $name) {
+               if(strcasecmp($name, 'body') != 0) {
+                       HTMLDiffer::diffDebug( "Ending $name node.\n");
+                       if (0 == strcasecmp($name,'img')) {
+                               // Insert a dummy leaf for the image
+                               $img = new ImageNode($this->currentParent, $this->currentParent->attributes);
+                               $this->currentParent->children[] = $img;
+                               $img->whiteBefore = $this->whiteSpaceBeforeThis;
+                               $this->lastSibling = $img;
+                               $this->textNodes[] = $img;
+                       }
+                       $this->endWord();
+                       if (!in_array(strtolower($this->currentParent->qName),TagNode::$blocks)) {
+                               $this->lastSibling = $this->currentParent;
+                       } else {
+                               $this->lastSibling = new DummyNode();
+                       }
+                       $this->currentParent = $this->currentParent->parent;
+                       $this->whiteSpaceBeforeThis = false;
+                       if (!$this->notInPre && strcasecmp($name, 'pre') == 0) {
+                               $this->notInPre = true;
+                       }
+               } else {
+                       $this->endDocument();
+               }
+       }
+
+       const regex = '/([\s\.\,\"\\\'\(\)\?\:\;\!\{\}\-\+\*\=\_\[\]\&\|\$]{1})/';
+       const whitespace = '/^[\s]{1}$/';
+       const delimiter = '/^[\s\.\,\"\\\'\(\)\?\:\;\!\{\}\-\+\*\=\_\[\]\&\|\$]{1}$/';
+
+       public function characters($parser, $data) {
+               $matches = preg_split(self::regex, $data, -1, PREG_SPLIT_DELIM_CAPTURE);
+
+               foreach($matches as &$word) {
+                       if (preg_match(self::whitespace, $word) && $this->notInPre) {
+                               $this->endWord();
+                               $this->lastSibling->whiteAfter = true;
+                               $this->whiteSpaceBeforeThis = true;
+                       } else if (preg_match(self::delimiter, $word)) {
+                               $this->endWord();
+                               $textNode = new TextNode($this->currentParent, $word);
+                               $this->currentParent->children[] = $textNode;
+                               $textNode->whiteBefore = $this->whiteSpaceBeforeThis;
+                               $this->whiteSpaceBeforeThis = false;
+                               $this->lastSibling = $textNode;
+                               $this->textNodes[] = $textNode;
+                       } else {
+                               $this->newWord .= $word;
+                       }
+               }
+       }
+
+       private function endWord() {
+               if ($this->newWord !== '') {
+                       $node = new TextNode($this->currentParent, $this->newWord);
+                       $this->currentParent->children[] = $node;
+                       $node->whiteBefore = $this->whiteSpaceBeforeThis;
+                       $this->whiteSpaceBeforeThis = false;
+                       $this->lastSibling = $node;
+                       $this->textNodes[] = $node;
+                       $this->newWord = "";
+               }
+       }
+
+       public function getDiffLines() {
+               return array_map(array('TextNode','toDiffLine'), $this->textNodes);
+       }
+}
+
+class TextNodeDiffer {
+
+       private $textNodes;
+       public $bodyNode;
+
+       private $oldTextNodes;
+       private $oldBodyNode;
+
+       private $newID = 0;
+
+       private $changedID = 0;
+
+       private $changedIDUsed = false;
+
+       // used to remove the whitespace between a red and green block
+       private $whiteAfterLastChangedPart = false;
+
+       private $deletedID = 0;
+
+       function __construct(DomTreeBuilder $tree, DomTreeBuilder $oldTree) {
+               $this->textNodes = $tree->textNodes;
+               $this->bodyNode = $tree->bodyNode;
+               $this->oldTextNodes = $oldTree->textNodes;
+               $this->oldBodyNode = $oldTree->bodyNode;
+       }
+
+       public function markAsNew($start, $end) {
+               if ($end <= $start) {
+                       return;
+               }
+
+               if ($this->whiteAfterLastChangedPart) {
+                       $this->textNodes[$start]->whiteBefore = false;
+               }
+
+               for ($i = $start; $i < $end; ++$i) {
+                       $mod = new Modification(Modification::ADDED);
+                       $mod->id = $this->newID;
+                       $this->textNodes[$i]->modification = $mod;
+               }
+               if ($start < $end) {
+                       $this->textNodes[$start]->modification->firstOfID = true;
+               }
+               ++$this->newID;
+       }
+
+       public function handlePossibleChangedPart($leftstart, $leftend, $rightstart, $rightend) {
+               $i = $rightstart;
+               $j = $leftstart;
+
+               if ($this->changedIDUsed) {
+                       ++$this->changedID;
+                       $this->changedIDUsed = false;
+               }
+
+               $changes;
+               while ($i < $rightend) {
+                       $acthis = new AncestorComparator($this->textNodes[$i]->getParentTree());
+                       $acother = new AncestorComparator($this->oldTextNodes[$j]->getParentTree());
+                       $result = $acthis->getResult($acother);
+                       unset($acthis, $acother);
+
+                       if ($result->changed) {
+                               $mod = new Modification(Modification::CHANGED);
+
+                               if (!$this->changedIDUsed) {
+                                       $mod->firstOfID = true;
+                               } else if (!is_null($result->changes) && $result->changes !== $this->changes) {
+                                       ++$this->changedID;
+                                       $mod->firstOfID = true;
+                               }
+
+                               $mod->changes = $result->changes;
+                               $mod->id = $this->changedID;
+
+                               $this->textNodes[$i]->modification = $mod;
+                               $this->changes = $result->changes;
+                               $this->changedIDUsed = true;
+                       } else if ($this->changedIDUsed) {
+                               ++$this->changedID;
+                               $this->changedIDUsed = false;
+                       }
+                       ++$i;
+                       ++$j;
+               }
+       }
+
+       public function markAsDeleted($start, $end, $before) {
+
+               if ($end <= $start) {
+                       return;
+               }
+
+               if ($before > 0 && $this->textNodes[$before - 1]->whiteAfter) {
+                       $this->whiteAfterLastChangedPart = true;
+               } else {
+                       $this->whiteAfterLastChangedPart = false;
+               }
+
+               for ($i = $start; $i < $end; ++$i) {
+                       $mod = new Modification(Modification::REMOVED);
+                       $mod->id = $this->deletedID;
+
+                       // oldTextNodes is used here because we're going to move its deleted
+                       // elements to this tree!
+                       $this->oldTextNodes[$i]->modification = $mod;
+               }
+               $this->oldTextNodes[$start]->modification->firstOfID = true;
+
+               $root = $this->oldTextNodes[$start]->getLastCommonParent($this->oldTextNodes[$end-1])->parent;
+
+               $junk1 = $junk2 = null;
+               $deletedNodes = $root->getMinimalDeletedSet($this->deletedID, $junk1, $junk2);
+
+               HTMLDiffer::diffDebug( "Minimal set of deleted nodes of size " . count($deletedNodes) . "\n" );
+
+               // Set prevLeaf to the leaf after which the old HTML needs to be
+               // inserted
+               if ($before > 0) {
+                       $prevLeaf = $this->textNodes[$before - 1];
+               }
+               // Set nextLeaf to the leaf before which the old HTML needs to be
+               // inserted
+               if ($before < count($this->textNodes)) {
+                       $nextLeaf = $this->textNodes[$before];
+               }
+
+               while (count($deletedNodes) > 0) {
+                       if (isset($prevLeaf)) {
+                               $prevResult = $prevLeaf->getLastCommonParent($deletedNodes[0]);
+                       } else {
+                               $prevResult = new LastCommonParentResult();
+                               $prevResult->parent = $this->bodyNode;
+                               $prevResult->indexInLastCommonParent = 0;
+                       }
+                       if (isset($nextleaf)) {
+                               $nextResult = $nextLeaf->getLastCommonParent($deletedNodes[count($deletedNodes) - 1]);
+                       } else {
+                               $nextResult = new LastCommonParentResult();
+                               $nextResult->parent = $this->bodyNode;
+                               $nextResult->indexInLastCommonParent = $this->bodyNode->getNbChildren();
+                       }
+
+                       if ($prevResult->lastCommonParentDepth == $nextResult->lastCommonParentDepth) {
+                               // We need some metric to choose which way to add-...
+                               if ($deletedNodes[0]->parent === $deletedNodes[count($deletedNodes) - 1]->parent
+                                               && $prevResult->parent === $nextResult->parent) {
+                                       // The difference is not in the parent
+                                       $prevResult->lastCommonParentDepth = $prevResult->lastCommonParentDepth + 1;
+                               } else {
+                                       // The difference is in the parent, so compare them
+                                       // now THIS is tricky
+                                       $distancePrev = $deletedNodes[0]->parent->getMatchRatio($prevResult->parent);
+                                       $distanceNext = $deletedNodes[count($deletedNodes) - 1]->parent->getMatchRatio($nextResult->parent);
+
+                                       if ($distancePrev <= $distanceNext) {
+                                               $prevResult->lastCommonParentDepth = $prevResult->lastCommonParentDepth + 1;
+                                       } else {
+                                               $nextResult->lastCommonParentDepth = $nextResult->lastCommonParentDepth + 1;
+                                       }
+                               }
+
+                       }
+
+                       if ($prevResult->lastCommonParentDepth > $nextResult->lastCommonParentDepth) {
+                               // Inserting at the front
+                               if ($prevResult->splittingNeeded) {
+                                       $prevLeaf->parent->splitUntil($prevResult->parent, $prevLeaf, true);
+                               }
+                               $prevLeaf = $deletedNodes[0]->copyTree();
+                               unset($deletedNodes[0]);
+                               $deletedNodes = array_values($deletedNodes);
+                               $prevLeaf->setParent($prevResult->parent);
+                               $prevResult->parent->addChildAbsolute($prevLeaf,$prevResult->indexInLastCommonParent + 1);
+                       } else if ($prevResult->lastCommonParentDepth < $nextResult->lastCommonParentDepth) {
+                               // Inserting at the back
+                               if ($nextResult->splittingNeeded) {
+                                       $splitOccured = $nextLeaf->parent->splitUntil($nextResult->parent, $nextLeaf, false);
+                                       if ($splitOccured) {
+                                               // The place where to insert is shifted one place to the
+                                               // right
+                                               $nextResult->indexInLastCommonParent = $nextResult->indexInLastCommonParent + 1;
+                                       }
+                               }
+                               $nextLeaf = $deletedNodes[count(deletedNodes) - 1]->copyTree();
+                               unset($deletedNodes[count(deletedNodes) - 1]);
+                               $deletedNodes = array_values($deletedNodes);
+                               $nextLeaf->setParent($nextResult->parent);
+                               $nextResult->parent->addChildAbsolute($nextLeaf,$nextResult->indexInLastCommonParent);
+                       }
+               }
+               ++$this->deletedID;
+       }
+
+       public function expandWhiteSpace() {
+               $this->bodyNode->expandWhiteSpace();
+       }
+
+       public function lengthNew(){
+               return count($this->textNodes);
+       }
+
+       public function lengthOld(){
+               return count($this->oldTextNodes);
+       }
+}
+
+class HTMLDiffer {
+
+       private $output;
+       private static $debug = '';
+
+       function __construct($output) {
+               $this->output = $output;
+       }
+
+       function htmlDiff($from, $to) {
+               wfProfileIn( __METHOD__ );
+               // Create an XML parser
+               $xml_parser = xml_parser_create('');
+
+               $domfrom = new DomTreeBuilder();
+
+               // Set the functions to handle opening and closing tags
+               xml_set_element_handler($xml_parser, array($domfrom, "startElement"), array($domfrom, "endElement"));
+
+               // Set the function to handle blocks of character data
+               xml_set_character_data_handler($xml_parser, array($domfrom, "characters"));
+
+               HTMLDiffer::diffDebug( "Parsing " . strlen($from) . " characters worth of HTML\n" );
+               if (!xml_parse($xml_parser, '<?xml version="1.0" encoding="UTF-8"?>'.Sanitizer::hackDocType().'<body>', false)
+                                       || !xml_parse($xml_parser, $from, false)
+                                       || !xml_parse($xml_parser, '</body>', true)){
+                       $error = xml_error_string(xml_get_error_code($xml_parser));
+                       $line = xml_get_current_line_number($xml_parser);
+                       HTMLDiffer::diffDebug( "XML error: $error at line $line\n" );
+               }
+               xml_parser_free($xml_parser);
+               unset($from);
+
+               $xml_parser = xml_parser_create('');
+
+               $domto = new DomTreeBuilder();
+
+               // Set the functions to handle opening and closing tags
+               xml_set_element_handler($xml_parser, array($domto, "startElement"), array($domto, "endElement"));
+
+               // Set the function to handle blocks of character data
+               xml_set_character_data_handler($xml_parser, array($domto, "characters"));
+
+               HTMLDiffer::diffDebug( "Parsing " . strlen($to) . " characters worth of HTML\n" );
+               if (!xml_parse($xml_parser, '<?xml version="1.0" encoding="UTF-8"?>'.Sanitizer::hackDocType().'<body>', false)
+               || !xml_parse($xml_parser, $to, false)
+               || !xml_parse($xml_parser, '</body>', true)){
+                       $error = xml_error_string(xml_get_error_code($xml_parser));
+                       $line = xml_get_current_line_number($xml_parser);
+                       HTMLDiffer::diffDebug( "XML error: $error at line $line\n" );
+               }
+               xml_parser_free($xml_parser);
+               unset($to);
+
+               $diffengine = new WikiDiff3();
+               $differences = $this->preProcess($diffengine->diff_range($domfrom->getDiffLines(), $domto->getDiffLines()));
+               unset($xml_parser, $diffengine);
+
+               $domdiffer = new TextNodeDiffer($domto, $domfrom);
+
+               $currentIndexLeft = 0;
+               $currentIndexRight = 0;
+               foreach ($differences as &$d) {
+                       if ($d->leftstart > $currentIndexLeft) {
+                               $domdiffer->handlePossibleChangedPart($currentIndexLeft, $d->leftstart,
+                                       $currentIndexRight, $d->rightstart);
+                       }
+                       if ($d->leftlength > 0) {
+                               $domdiffer->markAsDeleted($d->leftstart, $d->leftend, $d->rightstart);
+                       }
+                       $domdiffer->markAsNew($d->rightstart, $d->rightend);
+
+                       $currentIndexLeft = $d->leftend;
+                       $currentIndexRight = $d->rightend;
+               }
+               $oldLength = $domdiffer->lengthOld();
+               if ($currentIndexLeft < $oldLength) {
+                       $domdiffer->handlePossibleChangedPart($currentIndexLeft, $oldLength, $currentIndexRight, $domdiffer->lengthNew());
+               }
+               $domdiffer->expandWhiteSpace();
+               $output = new HTMLOutput('htmldiff', $this->output);
+               $output->parse($domdiffer->bodyNode);
+               wfProfileOut( __METHOD__ );
+       }
+
+       private function preProcess(/*array*/ $differences) {
+               $newRanges = array();
+
+               $nbDifferences = count($differences);
+               for ($i = 0; $i < $nbDifferences; ++$i) {
+                       $leftStart = $differences[$i]->leftstart;
+                       $leftEnd = $differences[$i]->leftend;
+                       $rightStart = $differences[$i]->rightstart;
+                       $rightEnd = $differences[$i]->rightend;
+
+                       $leftLength = $leftEnd - $leftStart;
+                       $rightLength = $rightEnd - $rightStart;
+
+                       while ($i + 1 < $nbDifferences && self::score($leftLength,
+                                               $differences[$i + 1]->leftlength,
+                                               $rightLength,
+                                               $differences[$i + 1]->rightlength)
+                                       > ($differences[$i + 1]->leftstart - $leftEnd)) {
+                               $leftEnd = $differences[$i + 1]->leftend;
+                               $rightEnd = $differences[$i + 1]->rightend;
+                               $leftLength = $leftEnd - $leftStart;
+                               $rightLength = $rightEnd - $rightStart;
+                               ++$i;
+                       }
+                       $newRanges[] = new RangeDifference($leftStart, $leftEnd, $rightStart, $rightEnd);
+               }
+               return $newRanges;
+       }
+
+       /**
+        * Heuristic to merge differences for readability.
+        */
+       public static function score($ll, $nll, $rl, $nrl) {
+               if (($ll == 0 && $nll == 0)
+                               || ($rl == 0 && $nrl == 0)) {
+                       return 0;
+               }
+               $numbers = array($ll, $nll, $rl, $nrl);
+               $d = 0;
+               foreach ($numbers as &$number) {
+                       while ($number > 3) {
+                               $d += 3;
+                               $number -= 3;
+                               $number *= 0.5;
+                       }
+                       $d += $number;
+
+               }
+               return $d / (1.5 * count($numbers));
+       }
+
+       /**
+        * Add to debug output
+        * @param string $str Debug output
+        */
+       public static function diffDebug( $str ) {
+               self :: $debug .= $str;
+       }
+       
+       /**
+        * Get debug output
+        * @return string
+        */
+       public static function getDebugOutput() {
+               return self :: $debug;
+       }
+
+}
+
+class TextOnlyComparator {
+
+       public $leafs = array();
+
+       function _construct(TagNode $tree) {
+               $this->addRecursive($tree);
+               $this->leafs = array_map(array('TextNode','toDiffLine'), $this->leafs);
+       }
+
+       private function addRecursive(TagNode $tree) {
+               foreach ($tree->children as &$child) {
+                       if ($child instanceof TagNode) {
+                               $this->addRecursive($child);
+                       } else if ($child instanceof TextNode) {
+                               $this->leafs[] = $node;
+                       }
+               }
+       }
+
+       public function getMatchRatio(TextOnlyComparator $other) {
+               $nbOthers = count($other->leafs);
+               $nbThis = count($this->leafs);
+               if($nbOthers == 0 || $nbThis == 0){
+                       return -log(0);
+               }
+
+               $diffengine = new WikiDiff3(25000, 1.35);
+               $diffengine->diff($this->leafs, $other->leafs);
+
+               $lcsLength = $diffengine->getLcsLength();
+
+               $distanceThis = $nbThis-$lcsLength;
+
+               return (2.0 - $lcsLength/$nbOthers - $lcsLength/$nbThis) / 2.0;
+       }
+}
+
+class AncestorComparatorResult {
+
+       public $changed = false;
+
+       public $changes = "";
+}
+
+/**
+ * A comparator used when calculating the difference in ancestry of two Nodes.
+ */
+class AncestorComparator {
+
+       public $ancestors;
+       public $ancestorsText;
+
+       function __construct(/*array*/ $ancestors) {
+               $this->ancestors = $ancestors;
+               $this->ancestorsText = array_map(array('TagNode','toDiffLine'), $ancestors);
+       }
+
+       public $compareTxt = "";
+
+       public function getResult(AncestorComparator $other) {
+               $result = new AncestorComparatorResult();
+
+               $diffengine = new WikiDiff3(10000, 1.35);
+               $differences = $diffengine->diff_range($other->ancestorsText,$this->ancestorsText);
+
+               if (count($differences) == 0){
+                       return $result;
+               }
+               $changeTxt = new ChangeTextGenerator($this, $other);
+
+               $result->changed = true;
+               $result->changes = $changeTxt->getChanged($differences)->toString();
+
+               return $result;
+       }
+}
+
+class ChangeTextGenerator {
+
+       private $ancestorComparator;
+       private $other;
+
+       private $factory;
+
+       function __construct(AncestorComparator $ancestorComparator, AncestorComparator $other) {
+               $this->ancestorComparator = $ancestorComparator;
+               $this->other = $other;
+               $this->factory = new TagToStringFactory();
+       }
+
+       public function getChanged(/*array*/ $differences) {
+               $txt = new ChangeText;
+               $rootlistopened = false;
+               if (count($differences) > 1) {
+                       $txt->addHtml('<ul class="changelist">');
+                       $rootlistopened = true;
+               }
+               $nbDifferences = count($differences);
+               for ($j = 0; $j < $nbDifferences; ++$j) {
+                       $d = $differences[$j];
+                       $lvl1listopened = false;
+                       if ($rootlistopened) {
+                               $txt->addHtml('<li>');
+                       }
+                       if ($d->leftlength + $d->rightlength > 1) {
+                               $txt->addHtml('<ul class="changelist">');
+                               $lvl1listopened = true;
+                       }
+                       // left are the old ones
+                       for ($i = $d->leftstart; $i < $d->leftend; ++$i) {
+                               if ($lvl1listopened){
+                                       $txt->addHtml('<li>');
+                               }
+                               // add a bullet for a old tag
+                               $this->addTagOld($txt, $this->other->ancestors[$i]);
+                               if ($lvl1listopened){
+                                       $txt->addHtml('</li>');
+                               }
+                       }
+                       // right are the new ones
+                       for ($i = $d->rightstart; $i < $d->rightend; ++$i) {
+                               if ($lvl1listopened){
+                                       $txt->addHtml('<li>');
+                               }
+                               // add a bullet for a new tag
+                               $this->addTagNew($txt, $this->ancestorComparator->ancestors[$i]);
+
+                               if ($lvl1listopened){
+                                       $txt->addHtml('</li>');
+                               }
+                       }
+                       if ($lvl1listopened) {
+                               $txt->addHtml('</ul>');
+                       }
+                       if ($rootlistopened) {
+                               $txt->addHtml('</li>');
+                       }
+               }
+               if ($rootlistopened) {
+                       $txt->addHtml('</ul>');
+               }
+               return $txt;
+       }
+
+       private function addTagOld(ChangeText $txt, TagNode $ancestor) {
+               $this->factory->create($ancestor)->getRemovedDescription($txt);
+       }
+
+       private function addTagNew(ChangeText $txt, TagNode $ancestor) {
+               $this->factory->create($ancestor)->getAddedDescription($txt);
+       }
+}
+
+class ChangeText {
+
+       private $txt = "";
+
+       public function addHtml($s) {
+               $this->txt .= $s;
+       }
+
+       public function toString() {
+               return $this->txt;
+       }
+}
+
+class TagToStringFactory {
+
+       private static $containerTags = array('html', 'body', 'p', 'blockquote',
+               'h1', 'h2', 'h3', 'h4', 'h5', 'pre', 'div', 'ul', 'ol', 'li',
+               'table', 'tbody', 'tr', 'td', 'th', 'br', 'hr', 'code', 'dl',
+               'dt', 'dd', 'input', 'form', 'img', 'span', 'a');
+
+       private static $styleTags = array('i', 'b', 'strong', 'em', 'font',
+               'big', 'del', 'tt', 'sub', 'sup', 'strike');
+
+       const MOVED = 1;
+       const STYLE = 2;
+       const UNKNOWN = 4;
+
+       public function create(TagNode $node) {
+               $sem = $this->getChangeSemantic($node->qName);
+               if (strcasecmp($node->qName,'a') == 0) {
+                       return new AnchorToString($node, $sem);
+               }
+               if (strcasecmp($node->qName,'img') == 0) {
+                       return new NoContentTagToString($node, $sem);
+               }
+               return new TagToString($node, $sem);
+       }
+
+       protected function getChangeSemantic($qname) {
+               if (in_array(strtolower($qname),self::$containerTags)) {
+                       return self::MOVED;
+               }
+               if (in_array(strtolower($qname),self::$styleTags)) {
+                       return self::STYLE;
+               }
+               return self::UNKNOWN;
+       }
+}
+
+class TagToString {
+
+       protected $node;
+
+       protected $sem;
+
+       function __construct(TagNode $node, $sem) {
+               $this->node = $node;
+               $this->sem = $sem;
+       }
+
+       public function getRemovedDescription(ChangeText $txt) {
+               $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' );
+               if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){
+                       $tagDescription = "&lt;" . $this->node->qName . "&gt;";
+               }
+               if ($this->sem == TagToStringFactory::MOVED) {
+                       $txt->addHtml( wfMsgExt( 'diff-movedoutof', 'parseinline', $tagDescription ) );
+               } else if ($this->sem == TagToStringFactory::STYLE) {
+                       $txt->addHtml($tagDescription . ' ' . wfMsgExt( 'diff-styleremoved' , 'parseinline' ) );
+               } else {
+                       $txt->addHtml($tagDescription . ' ' . wfMsgExt( 'diff-removed' , 'parseinline' ) );
+               }
+               $this->addAttributes($txt, $this->node->attributes);
+               $txt->addHtml('.');
+       }
+
+       public function getAddedDescription(ChangeText $txt) {
+               $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' );
+               if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){
+                       $tagDescription = "&lt;" . $this->node->qName . "&gt;";
+               }
+               if ($this->sem == TagToStringFactory::MOVED) {
+                       $txt->addHtml( wfMsgExt( 'diff-movedto' , 'parseinline', $tagDescription) );
+               } else if ($this->sem == TagToStringFactory::STYLE) {
+                       $txt->addHtml($tagDescription . ' ' . wfMsgExt( 'diff-styleadded', 'parseinline' ) );
+               } else {
+                       $txt->addHtml($tagDescription . ' ' . wfMsgExt( 'diff-added', 'parseinline' ) );
+               }
+               $this->addAttributes($txt, $this->node->attributes);
+               $txt->addHtml('.');
+       }
+
+       protected function addAttributes(ChangeText $txt, array $attributes) {
+               if (count($attributes) < 1) {
+                       return;
+               }
+               $firstOne = true;
+               $nbAttributes_min_1 = count($attributes)-1;
+               $keys = array_keys($attributes);
+               for ($i=0;$i<$nbAttributes_min_1;$i++) {
+                       $key = $keys[$i];
+                       $attr = $attributes[$key];
+                       if($firstOne) {
+                               $firstOne = false;
+                               $txt->addHtml( wfMsgExt('diff-with', 'escapenoentities', $this->translateArgument($key), htmlspecialchars($attr) ) );
+                               continue;
+                       }
+                       $txt->addHtml( wfMsgExt( 'comma-separator', 'escapenoentities' ) .
+                               wfMsgExt( 'diff-with-additional', 'escapenoentities',
+                               $this->translateArgument( $key ), htmlspecialchars( $attr ) )
+                       );
+               }
+
+               if ($nbAttributes_min_1 > 0) {
+                       $txt->addHtml( wfMsgExt( 'diff-with-final', 'escapenoentities',
+                       $this->translateArgument($keys[$nbAttributes_min_1]),
+                       htmlspecialchars($attributes[$keys[$nbAttributes_min_1]]) ) );
+               }
+       }
+
+       protected function translateArgument($name) {
+               $translation = wfMsgExt('diff-' . $name, 'parseinline' );
+               if ( wfEmptyMsg( 'diff-' . $name, $translation ) ) {
+                       $translation = "&lt;" . $name . "&gt;";;
+               }
+               return htmlspecialchars( $translation );
+       }
+}
+
+class NoContentTagToString extends TagToString {
+
+       function __construct(TagNode $node, $sem) {
+               parent::__construct($node, $sem);
+       }
+
+       public function getAddedDescription(ChangeText $txt) {
+               $tagDescription = wfMsgExt('diff-' . $this->node->qName, 'parseinline' );
+               if( wfEmptyMsg( 'diff-' . $this->node->qName, $tagDescription ) ){
+                       $tagDescription = "&lt;" . $this->node->qName . "&gt;";
+               }
+               $txt->addHtml( wfMsgExt('diff-changedto', 'parseinline' ) . ' ' . $tagDescription);
+               $this->addAttributes($txt, $this->node->attributes);
+               $txt->addHtml('.');
+       }
+
+       public function getRemovedDescription(ChangeText $txt) {
+               $txt->addHtml( wfMsgExt('diff-changedfrom', 'parseinline' ) . ' ' . $tagDescription);
+               $this->addAttributes($txt, $this->node->attributes);
+               $txt->addHtml('.');
+       }
+}
+
+class AnchorToString extends TagToString {
+
+       function __construct(TagNode $node, $sem) {
+               parent::__construct($node, $sem);
+       }
+
+       protected function addAttributes(ChangeText $txt, array $attributes) {
+               if (array_key_exists('href', $attributes)) {
+                       $txt->addHtml(' ' . wfMsgExt( 'diff-withdestination', 'parseinline' ) . ' ' . htmlspecialchars($attributes['href']));
+                       unset($attributes['href']);
+               }
+               parent::addAttributes($txt, $attributes);
+       }
+}
+
+/**
+ * Takes a branch root and creates an HTML file for it.
+ */
+class HTMLOutput{
+
+       private $prefix;
+       private $handler;
+
+       function __construct($prefix, $handler) {
+               $this->prefix = $prefix;
+               $this->handler = $handler;
+       }
+
+       public function parse(TagNode $node) {
+               $handler = &$this->handler;
+
+               if (strcasecmp($node->qName, 'img') != 0 && strcasecmp($node->qName, 'body') != 0) {
+                       $handler->startElement($node->qName, $node->attributes);
+               }
+
+               $newStarted = false;
+               $remStarted = false;
+               $changeStarted = false;
+               $changeTXT = '';
+
+               foreach ($node->children as &$child) {
+                       if ($child instanceof TagNode) {
+                               if ($newStarted) {
+                                       $handler->endElement('span');
+                                       $newStarted = false;
+                               } else if ($changeStarted) {
+                                       $handler->endElement('span');
+                                       $changeStarted = false;
+                               } else if ($remStarted) {
+                                       $handler->endElement('span');
+                                       $remStarted = false;
+                               }
+                               $this->parse($child);
+                       } else if ($child instanceof TextNode) {
+                               $mod = $child->modification;
+
+                               if ($newStarted && ($mod->type != Modification::ADDED || $mod->firstOfID)) {
+                                       $handler->endElement('span');
+                                       $newStarted = false;
+                               } else if ($changeStarted && ($mod->type != Modification::CHANGED
+                                               || $mod->changes != $changeTXT || $mod->firstOfID)) {
+                                       $handler->endElement('span');
+                                       $changeStarted = false;
+                               } else if ($remStarted && ($mod->type != Modification::REMOVED || $mod ->firstOfID)) {
+                                       $handler->endElement('span');
+                                       $remStarted = false;
+                               }
+
+                               // no else because a removed part can just be closed and a new
+                               // part can start
+                               if (!$newStarted && $mod->type == Modification::ADDED) {
+                                       $attrs = array('class' => 'diff-html-added');
+                                       if ($mod->firstOfID) {
+                                               $attrs['id'] = "added-{$this->prefix}-{$mod->id}";
+                                       }
+                                       $handler->startElement('span', $attrs);
+                                       $newStarted = true;
+                               } else if (!$changeStarted && $mod->type == Modification::CHANGED) {
+                                       $attrs = array('class' => 'diff-html-changed');
+                                       if ($mod->firstOfID) {
+                                               $attrs['id'] = "changed-{$this->prefix}-{$mod->id}";
+                                       }
+                                       $handler->startElement('span', $attrs);
+
+                                       //tooltip
+                                       $handler->startElement('span', array('class' => 'tip'));
+                                       $handler->html($mod->changes);
+                                       $handler->endElement('span');
+
+                                       $changeStarted = true;
+                                       $changeTXT = $mod->changes;
+                               } else if (!$remStarted && $mod->type == Modification::REMOVED) {
+                                       $attrs = array('class'=>'diff-html-removed');
+                                       if ($mod->firstOfID) {
+                                               $attrs['id'] = "removed-{$this->prefix}-{$mod->id}";
+                                       }
+                                       $handler->startElement('span', $attrs);
+                                       $remStarted = true;
+                               }
+
+                               $chars = $child->text;
+
+                               if ($child instanceof ImageNode) {
+                                       $this->writeImage($child);
+                               } else {
+                                       $handler->characters($chars);
+                               }
+                       }
+               }
+
+               if ($newStarted) {
+                       $handler->endElement('span');
+                       $newStarted = false;
+               } else if ($changeStarted) {
+                       $handler->endElement('span');
+                       $changeStarted = false;
+               } else if ($remStarted) {
+                       $handler->endElement('span');
+                       $remStarted = false;
+               }
+
+               if (strcasecmp($node->qName, 'img') != 0
+                               && strcasecmp($node->qName, 'body') != 0) {
+                       $handler->endElement($node->qName);
+               }
+       }
+
+       private function writeImage(ImageNode  $imgNode) {
+               $attrs = $imgNode->attributes;
+               $this->handler->startElement('img', $attrs);
+               $this->handler->endElement('img');
+       }
+}
+
+class EchoingContentHandler {
+
+       function startElement($qname, /*array*/ $arguments) {
+               echo Xml::openElement($qname, $arguments);
+       }
+
+       function endElement($qname){
+               echo Xml::closeElement($qname);
+       }
+
+       function characters($chars){
+               echo htmlspecialchars($chars);
+       }
+
+       function html($html){
+               echo $html;
+       }
+
+}
+
+class DelegatingContentHandler {
+
+       private $delegate;
+
+       function __construct($delegate) {
+               $this->delegate = $delegate;
+       }
+
+       function startElement($qname, /*array*/ $arguments) {
+               $this->delegate->addHtml(Xml::openElement($qname, $arguments));
+       }
+
+       function endElement($qname){
+               $this->delegate->addHtml(Xml::closeElement($qname));
+       }
+
+       function characters($chars){
+               $this->delegate->addHtml(htmlspecialchars($chars));
+       }
+
+       function html($html){
+               $this->delegate->addHtml($html);
+       }
+}
diff --git a/includes/diff/Nodes.php b/includes/diff/Nodes.php
new file mode 100644 (file)
index 0000000..0f0dbdb
--- /dev/null
@@ -0,0 +1,430 @@
+<?php
+
+/** Copyright (C) 2008 Guy Van den Broeck <guy@guyvdb.eu>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ * or see http://www.gnu.org/
+ * 
+ * @ingroup DifferenceEngine
+ */
+/**
+ * Any element in the DOM tree of an HTML document.
+ */
+class Node {
+
+       public $parent;
+
+       protected $parentTree;
+
+       public $whiteBefore = false;
+
+       public $whiteAfter = false;
+
+       function __construct($parent) {
+               $this->parent = $parent;
+       }
+
+       public function getParentTree() {
+               if (!isset($this->parentTree)) {
+                       if (!is_null($this->parent)) {
+                               $this->parentTree = $this->parent->getParentTree();
+                               $this->parentTree[] = $this->parent;
+                       } else {
+                               $this->parentTree = array();
+                       }
+               }
+               return $this->parentTree;
+       }
+
+       public function getLastCommonParent(Node $other) {
+               $result = new LastCommonParentResult();
+
+               $myParents = $this->getParentTree();
+               $otherParents = $other->getParentTree();
+
+               $i = 1;
+               $isSame = true;
+               $nbMyParents = count($myParents);
+               $nbOtherParents = count($otherParents);
+               while ($isSame && $i < $nbMyParents && $i < $nbOtherParents) {
+                       if (!$myParents[$i]->openingTag === $otherParents[$i]->openingTag) {
+                               $isSame = false;
+                       } else {
+                               // After a while, the index i-1 must be the last common parent
+                               $i++;
+                       }
+               }
+
+               $result->lastCommonParentDepth = $i - 1;
+               $result->parent = $myParents[$i - 1];
+
+               if (!$isSame || $nbMyParents > $nbOtherParents) {
+                       // Not all tags matched, or all tags matched but
+                       // there are tags left in this tree
+                       $result->indexInLastCommonParent = $myParents[$i - 1]->getIndexOf($myParents[$i]);
+                       $result->splittingNeeded = true;
+               } else if ($nbMyParents <= $nbOtherParents) {
+                       $result->indexInLastCommonParent = $myParents[$i - 1]->getIndexOf($this);
+               }
+               return $result;
+       }
+
+       public function setParent($parent) {
+               $this->parent = $parent;
+               unset($this->parentTree);
+       }
+
+       public function inPre() {
+               $tree = $this->getParentTree();
+               foreach ($tree as &$ancestor) {
+                       if ($ancestor->isPre()) {
+                               return true;
+                       }
+               }
+               return false;
+       }
+}
+
+/**
+ * Node that can contain other nodes. Represents an HTML tag.
+ */
+class TagNode extends Node {
+
+       public $children = array();
+
+       public $qName;
+
+       public $attributes = array();
+
+       public $openingTag;
+
+       function __construct($parent, $qName, /*array*/ $attributes) {
+               parent::__construct($parent);
+               $this->qName = strtolower($qName);
+               foreach($attributes as $key => &$value){
+                       $this->attributes[strtolower($key)] = $value;
+               }
+               return $this->openingTag = Xml::openElement($this->qName, $this->attributes);
+       }
+
+       public function addChildAbsolute(Node $node, $index) {
+               array_splice($this->children, $index, 0, array($node));
+       }
+
+       public function getIndexOf(Node $child) {
+               // don't trust array_search with objects
+               foreach ($this->children as $key => &$value){
+                       if ($value === $child) {
+                               return $key;
+                       }
+               }
+               return null;
+       }
+
+       public function getNbChildren() {
+               return count($this->children);
+       }
+
+       public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) {
+               $nodes = array();
+
+               $allDeleted = false;
+               $somethingDeleted = false;
+               $hasNonDeletedDescendant = false;
+
+               if (empty($this->children)) {
+                       return $nodes;
+               }
+
+               foreach ($this->children as &$child) {
+                       $allDeleted_local = false;
+                       $somethingDeleted_local = false;
+                       $childrenChildren = $child->getMinimalDeletedSet($id, $allDeleted_local, $somethingDeleted_local);
+                       if ($somethingDeleted_local) {
+                               $nodes = array_merge($nodes, $childrenChildren);
+                               $somethingDeleted = true;
+                       }
+                       if (!$allDeleted_local) {
+                               $hasNonDeletedDescendant = true;
+                       }
+               }
+               if (!$hasNonDeletedDescendant) {
+                       $nodes = array($this);
+                       $allDeleted = true;
+               }
+               return $nodes;
+       }
+
+       public function splitUntil(TagNode $parent, Node $split, $includeLeft) {
+               $splitOccured = false;
+               if ($parent !== $this) {
+                       $part1 = new TagNode(null, $this->qName, $this->attributes);
+                       $part2 = new TagNode(null, $this->qName, $this->attributes);
+                       $part1->setParent($this->parent);
+                       $part2->setParent($this->parent);
+
+                       $onSplit = false;
+                       $pastSplit = false;
+                       foreach ($this->children as &$child)
+                       {
+                               if ($child === $split) {
+                                       $onSplit = true;
+                               }
+                               if(!$pastSplit || ($onSplit && $includeLeft)) {
+                                       $child->setParent($part1);
+                                       $part1->children[] = $child;
+                               } else {
+                                       $child->setParent($part2);
+                                       $part2->children[] = $child;
+                               }
+                               if ($onSplit) {
+                                       $onSplit = false;
+                                       $pastSplit = true;
+                               }
+                       }
+                       $myindexinparent = $this->parent->getIndexOf($this);
+                       if (!empty($part1->children)) {
+                               $this->parent->addChildAbsolute($part1, $myindexinparent);
+                       }
+                       if (!empty($part2->children)) {
+                               $this->parent->addChildAbsolute($part2, $myindexinparent);
+                       }
+                       if (!empty($part1->children) && !empty($part2->children)) {
+                               $splitOccured = true;
+                       }
+
+                       $this->parent->removeChild($myindexinparent);
+
+                       if ($includeLeft) {
+                               $this->parent->splitUntil($parent, $part1, $includeLeft);
+                       } else {
+                               $this->parent->splitUntil($parent, $part2, $includeLeft);
+                       }
+               }
+               return $splitOccured;
+
+       }
+
+       private function removeChild($index) {
+               unset($this->children[$index]);
+               $this->children = array_values($this->children);
+       }
+
+       public static $blocks = array('html', 'body','p','blockquote', 'h1',
+               'h2', 'h3', 'h4', 'h5', 'pre', 'div', 'ul', 'ol', 'li', 'table',
+               'tbody', 'tr', 'td', 'th', 'br');
+
+       public function copyTree() {
+               $newThis = new TagNode(null, $this->qName, $this->attributes);
+               $newThis->whiteBefore = $this->whiteBefore;
+               $newThis->whiteAfter = $this->whiteAfter;
+               foreach ($this->children as &$child) {
+                       $newChild = $child->copyTree();
+                       $newChild->setParent($newThis);
+                       $newThis->children[] = $newChild;
+               }
+               return $newThis;
+       }
+
+       public function getMatchRatio(TagNode $other) {
+               $txtComp = new TextOnlyComparator($other);
+               return $txtComp->getMatchRatio(new TextOnlyComparator($this));
+       }
+
+       public function expandWhiteSpace() {
+               $shift = 0;
+               $spaceAdded = false;
+
+               $nbOriginalChildren = $this->getNbChildren();
+               for ($i = 0; $i < $nbOriginalChildren; ++$i) {
+                       $child = $this->children[$i + $shift];
+
+                       if ($child instanceof TagNode) {
+                               if (!$child->isPre()) {
+                                       $child->expandWhiteSpace();
+                               }
+                       }
+                       if (!$spaceAdded && $child->whiteBefore) {
+                               $ws = new WhiteSpaceNode(null, ' ', $child->getLeftMostChild());
+                               $ws->setParent($this);
+                               $this->addChildAbsolute($ws,$i + ($shift++));
+                       }
+                       if ($child->whiteAfter) {
+                               $ws = new WhiteSpaceNode(null, ' ', $child->getRightMostChild());
+                               $ws->setParent($this);
+                               $this->addChildAbsolute($ws,$i + 1 + ($shift++));
+                               $spaceAdded = true;
+                       } else {
+                               $spaceAdded = false;
+                       }
+
+               }
+       }
+
+       public function getLeftMostChild() {
+               if (empty($this->children)) {
+                       return $this;
+               }
+               return $this->children[0]->getLeftMostChild();
+       }
+
+       public function getRightMostChild() {
+               if (empty($this->children)) {
+                       return $this;
+               }
+               return $this->children[$this->getNbChildren() - 1]->getRightMostChild();
+       }
+
+       public function isPre() {
+               return 0 == strcasecmp($this->qName,'pre');
+       }
+
+       public static function toDiffLine(TagNode $node) {
+               return $node->openingTag;
+       }
+}
+
+/**
+ * Represents a piece of text in the HTML file.
+ */
+class TextNode extends Node {
+
+       public $text;
+
+       public $modification;
+
+       function __construct($parent, $text) {
+               parent::__construct($parent);
+               $this->modification = new Modification(Modification::NONE);
+               $this->text = $text;
+       }
+
+       public function copyTree() {
+               $clone = clone $this;
+               $clone->setParent(null);
+               return $clone;
+       }
+
+       public function getLeftMostChild() {
+               return $this;
+       }
+
+       public function getRightMostChild() {
+               return $this;
+       }
+
+       public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) {
+               if ($this->modification->type == Modification::REMOVED
+                                       && $this->modification->id == $id){
+                       $somethingDeleted = true;
+                       $allDeleted = true;
+                       return array($this);
+               }
+               return array();
+       }
+
+       public function isSameText($other) {
+               if (is_null($other) || ! $other instanceof TextNode) {
+                       return false;
+               }
+               return str_replace('\n', ' ',$this->text) === str_replace('\n', ' ',$other->text);
+       }
+
+       public static function toDiffLine(TextNode $node) {
+               return str_replace('\n', ' ',$node->text);
+       }
+}
+
+class WhiteSpaceNode extends TextNode {
+
+       function __construct($parent, $s, Node $like = null) {
+               parent::__construct($parent, $s);
+               if(!is_null($like) && $like instanceof TextNode) {
+                       $newModification = clone $like->modification;
+                       $newModification->firstOfID = false;
+                       $this->modification = $newModification;
+               }
+       }
+}
+
+/**
+ * Represents the root of a HTML document.
+ */
+class BodyNode extends TagNode {
+
+       function __construct() {
+               parent::__construct(null, 'body', array());
+       }
+
+       public function copyTree() {
+               $newThis = new BodyNode();
+               foreach ($this->children as &$child) {
+                       $newChild = $child->copyTree();
+                       $newChild->setParent($newThis);
+                       $newThis->children[] = $newChild;
+               }
+               return $newThis;
+       }
+
+       public function getMinimalDeletedSet($id, &$allDeleted, &$somethingDeleted) {
+               $nodes = array();
+               foreach ($this->children as &$child) {
+                       $childrenChildren = $child->getMinimalDeletedSet($id,
+                                               $allDeleted, $somethingDeleted);
+                       $nodes = array_merge($nodes, $childrenChildren);
+               }
+               return $nodes;
+       }
+
+}
+
+/**
+ * Represents an image in HTML. Even though images do not contain any text they
+ * are independent visible objects on the page. They are logically a TextNode.
+ */
+class ImageNode extends TextNode {
+
+       public $attributes;
+
+       function __construct(TagNode $parent, /*array*/ $attrs) {
+               if(!array_key_exists('src', $attrs)) {
+                       HTMLDiffer::diffDebug( "Image without a source\n" );
+                       parent::__construct($parent, '<img></img>');
+               }else{
+                       parent::__construct($parent, '<img>' . strtolower($attrs['src']) . '</img>');
+               }
+               $this->attributes = $attrs;
+       }
+
+       public function isSameText($other) {
+               if (is_null($other) || ! $other instanceof ImageNode) {
+                       return false;
+               }
+               return $this->text === $other->text;
+       }
+
+}
+
+/**
+ * No-op node
+ */
+class DummyNode extends Node {
+
+       function __construct() {
+               // no op
+       }
+
+}